├── .gitignore ├── MFS 33273 NOSA.doc ├── PyGNSS.egg-info ├── PKG-INFO ├── SOURCES.txt ├── dependency_links.txt └── top_level.txt ├── README.md ├── build └── lib │ └── pygnss │ ├── __init__.py │ ├── e2es.py │ └── orbit.py ├── dist └── PyGNSS-0.7-py3.6.egg ├── notebooks ├── CYGNSS_L2_Python_Module_Demo-NewE2ES.ipynb └── CYGNSS_L2_Python_Module_Demo.ipynb ├── pygnss ├── __init__.py ├── e2es.py └── orbit.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | notebooks/.ipynb_checkpoints 3 | notebooks/*.png 4 | *.pyc 5 | *.png 6 | notebooks/*.gif 7 | *.gif 8 | old 9 | release 10 | notebooks/CYGNSS_L2_Python_Module_Demo.pdf 11 | notebooks/CYGNSS_L2_Python_Module_Demo.html 12 | notebooks/CYGNSS_L2_Python_Module_Testing.ipynb 13 | notebooks/CYGNSS_L2_Python_Module_Tyler.ipynb 14 | notebooks/CYGNSS_L2_Python_GPS_ID.ipynb 15 | notebooks/CYGNSS_L2_Python_RCG.ipynb 16 | notebooks/CYGNSS_Subsection_Development.ipynb 17 | -------------------------------------------------------------------------------- /MFS 33273 NOSA.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasa/PyGNSS/fae99c4fec1073a72dc1bfcd63da38a7376c0e67/MFS 33273 NOSA.doc -------------------------------------------------------------------------------- /PyGNSS.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.1 2 | Name: PyGNSS 3 | Version: 0.7 4 | Summary: Python Interface to Cyclone Global Navigation Satellite System (CYGNSS) Wind Dataset 5 | Home-page: UNKNOWN 6 | Author: Timothy Lang 7 | Author-email: timothy.j.lang@nasa.gov 8 | License: UNKNOWN 9 | Description-Content-Type: UNKNOWN 10 | Description: UNKNOWN 11 | Platform: UNKNOWN 12 | Classifier: Development Status :: 3 - Alpha 13 | Classifier: Environment :: Console 14 | -------------------------------------------------------------------------------- /PyGNSS.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | setup.py 2 | PyGNSS.egg-info/PKG-INFO 3 | PyGNSS.egg-info/SOURCES.txt 4 | PyGNSS.egg-info/dependency_links.txt 5 | PyGNSS.egg-info/top_level.txt 6 | pygnss/__init__.py 7 | pygnss/e2es.py 8 | pygnss/orbit.py -------------------------------------------------------------------------------- /PyGNSS.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /PyGNSS.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | pygnss 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This module enables the ingest, analysis, and plotting of Cyclone Global Navigation Satellite System (CYGNSS) on-orbit data as well as pre-launch CYGNSS End-to-End Simulator (E2ES) data. 2 | 3 | Notable features include the ability to identify contiguous tracks of specular reflections associated with the same pair of CYGNSS and Global Positioning System (GPS) satellites. The winds along these tracks can then be filtered to reduce noise. Precipitation from the Global Precipitation Measurement (GPM) constellation can also be added. 4 | 5 | Code example: 6 | ``` 7 | cyg = pygnss.orbit.read_cygnss_l2(files[0]) 8 | for sat in range(8): 9 | trl = pygnss.orbit.get_tracks(cyg, sat, verbose=True, eps=2.0) 10 | print('\nAdding IMERG to', len(trl), 'tracks') 11 | trl = pygnss.orbit.add_imerg(trl, ifiles, dt_imerg) 12 | print('Saving Files') 13 | pygnss.orbit.write_netcdfs(trl, tr_path + sdate + '/') 14 | cyg.close() 15 | ``` 16 | 17 | References 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /build/lib/pygnss/__init__.py: -------------------------------------------------------------------------------- 1 | from . import e2es 2 | from . import orbit 3 | -------------------------------------------------------------------------------- /build/lib/pygnss/e2es.py: -------------------------------------------------------------------------------- 1 | """ 2 | Title/Version 3 | ------------- 4 | Python CYGNSS Toolkit (PyGNSS) 5 | pygnss v0.7 6 | Developed & tested with Python 2.7 and 3.4 7 | 8 | 9 | Author 10 | ------ 11 | Timothy Lang 12 | NASA MSFC 13 | timothy.j.lang@nasa.gov 14 | (256) 961-7861 15 | 16 | 17 | Overview 18 | -------- 19 | This module enables the ingest, analysis, and plotting of Cyclone Global 20 | Navigation Satellite System (CYGNSS) End-to-End Simulator (E2ES) input and 21 | output data. To use, place in PYTHONPATH and use the following import command: 22 | import pygnss 23 | 24 | 25 | Notes 26 | ----- 27 | Requires - numpy, matplotlib, Basemap, netCDF4, warnings, os, six, datetime, 28 | sklearn, copy 29 | 30 | 31 | Change Log 32 | ---------- 33 | v0.7 Major Changes (04/20/2016) 34 | 1. Added CygnssSubsection class to consolidate and simplify data subsectioning. 35 | Can be called independently, but is also used by the plotting methods in 36 | CygnssL2WindDisplay class. Moved the subsection_data and get_good_data_mask 37 | methods to this new class, and greatly modifed them to eliminate 38 | method returns. 39 | 2. Added ability to subsection data by CYGNSS and GPS satellite numbers, as 40 | well as by range-corrected gain threshold or interval. All plotting routines 41 | now support these subsectioning capabilities. 42 | 3. Added get_datetime indpendent function to derive datetime objects in the 43 | same array shape as the WindSpeed data. Added datetime module dependency. 44 | 4. Added CygnssTrack class to leverage CygnssSubsection to help isolate 45 | and contain all the data from an individual track. Also enables filtering 46 | of wind speeds along a track. 47 | 5. Added get_tracks independent function to isolate all individual tracks, and 48 | return a list of CygnssTrack objects from a CygnssSingleSat, CygnssMultiSat, 49 | or CygnssL2WindDisplay object. 50 | 51 | v0.6 Major Changes (11/20/2015) 52 | 1. Added hist2d_plot method to CygnssL2WindDisplay. 53 | 2. Added threshold keyword to allow filtering of histrogram figures by 54 | RangeCorrectedGain windows. 55 | 56 | v0.5 Major Changes (08/10/2015) 57 | 1. Supports Python 3 now. 58 | 59 | v0.4 Major Changes (07/02/2015) 60 | 1. Made all code pep8 compliant. 61 | 62 | v0.3 Major Changes (03/19/2015) 63 | 1. Documentation improvements. Doing help(pygnss) should be more useful now. 64 | 2. Fixed bug where GoodData attribute was not getting set for CygnssSingleSat 65 | objects after they were input into CygnssL2WindDisplay. 66 | 3. Fixes to ensure CygnssSingle/MultiSat classes can ingest L1 DDM files w/out 67 | errors. This provides a basis for adding L1 DDM functionality to PyGNSS. 68 | 4. Swapped out np.rank for np.ndim due to annoying deprecation warnings. 69 | 70 | v0.2 Major Changes (02/24/2015) 71 | 1. Fixed miscellaneous bugs related to data subsectioning and plotting. 72 | 2. Added histogram plot for CYGNSS vs. Truth winds. 73 | 74 | v0.1 Functionality: 75 | 1. Reads netCDFs for input to CYGNSS E2ES as well as output files. 76 | 2. Can ingest single-satellite data or merge all 8 together. 77 | 3. Capable of masking L2 wind data by RangeCorrectedGain. 78 | 4. Basic display objects & plotting routines exist for input/output data, w/ 79 | support for combined input/output plots. 80 | 81 | 82 | Planned Updates 83 | --------------- 84 | 1. Enable subsectioning of output data specifically by time rather than index. 85 | 2. Support for DDM file analysis/plotting 86 | 3. Merged input/output display object for 1-command combo plots given proper 87 | inputs. 88 | 4. Get CygnssL2WindDisplay.specular_plot() to automatically adjust size of 89 | points to reflect actual CYGNSS spatial resolution on Basemap. Right now, 90 | user just has manual control of the marker size and would need to guess at 91 | this if they want the specular points to be truly spatially accurate. 92 | 5. Incorporate land/ocean flag in non-Basemap CYGNSS plots 93 | 94 | """ 95 | from __future__ import print_function 96 | import numpy as np 97 | import matplotlib.pyplot as plt 98 | from mpl_toolkits.basemap import Basemap 99 | from netCDF4 import Dataset 100 | from warnings import warn 101 | import os 102 | from six import string_types 103 | import datetime as dt 104 | from sklearn.cluster import DBSCAN 105 | from copy import deepcopy 106 | 107 | VERSION = '0.7' 108 | 109 | ######################### 110 | 111 | 112 | class NetcdfFile(object): 113 | 114 | """Base class used for reading netCDF-format L1 and L2 CYGNSS data files""" 115 | 116 | def __init__(self, filename=None): 117 | try: 118 | self.read_netcdf(filename) 119 | except: 120 | warn('Please provide a correct filename as argument') 121 | 122 | def read_netcdf(self, filename): 123 | """variable_list = holds all the variable key strings """ 124 | volume = Dataset(filename, 'r') 125 | self.filename = os.path.basename(filename) 126 | self.fill_variables(volume) 127 | 128 | def fill_variables(self, volume): 129 | """Loop thru all variables and store them as attributes""" 130 | self.variable_list = [] 131 | for key in volume.variables.keys(): 132 | new_var = np.array(volume.variables[key][:]) 133 | setattr(self, key, new_var) 134 | self.variable_list.append(key) 135 | 136 | ######################### 137 | 138 | 139 | class CygnssSingleSat(NetcdfFile): 140 | 141 | """ 142 | Child class of NetcdfFile. Can ingest both L2 Wind and DDM files. 143 | All variables within the files are incorporated as attributes of the 144 | class. This class forms the main building block of PyGNSS. 145 | """ 146 | 147 | def get_gain_mask(self, number=4): 148 | """ 149 | With L2 wind data, identifies top specular points in terms of 150 | range corrected gain. Creates the GoodData attribute, which provides 151 | a mask that analysis and plotting routines can use to only consider 152 | specular points with the highest RangeCorrectedGain 153 | number = Number of specular points to consider in the rankings 154 | """ 155 | if hasattr(self, 'RangeCorrectedGain'): 156 | self.GoodData = 0 * np.int16(self.RangeCorrectedGain) 157 | indices = np.argsort(self.RangeCorrectedGain, axis=1) 158 | max4 = indices[:, -1*number:] 159 | for i in np.arange(np.shape(self.RangeCorrectedGain)[0]): 160 | self.GoodData[i, max4[i]] = 1 161 | self.variable_list.append('GoodData') 162 | 163 | ######################### 164 | 165 | 166 | class CygnssMultiSat(object): 167 | 168 | """ 169 | Can ingest both L2 Wind and L1 DDM files. Merges the CYGNSS constellation's 170 | individual satellites' data together into a class structure very similar to 171 | CygnssSingleSat, just with bigger array dimensions. 172 | """ 173 | 174 | def __init__(self, l2list, number=4): 175 | """ 176 | l2list = list of CygnssL2SingleSat objects or files 177 | number = Number of maximum RangeCorrectedGain slots to consider 178 | """ 179 | warntxt = 'Requires input list of CygnssSingleSat ' + \ 180 | 'objects or L2 wind files' 181 | try: 182 | test = l2list[0].WindSpeed 183 | except: 184 | try: 185 | if isinstance(l2list[0], string_types): 186 | tmplist = [] 187 | for filen in l2list: 188 | sat = CygnssSingleSat(filen) 189 | sat.get_gain_mask(number=number) 190 | tmplist.append(sat) 191 | l2list = tmplist 192 | else: 193 | warn(warntxt) 194 | return 195 | except: 196 | warn(warntxt) 197 | return 198 | self.satellites = l2list 199 | self.merge_cygnss_data() 200 | 201 | def merge_cygnss_data(self): 202 | """ 203 | Loop over each satellite and append its data to the master arrays 204 | """ 205 | for i, sat in enumerate(self.satellites): 206 | if i == 0: 207 | self.variable_list = sat.variable_list 208 | for var in sat.variable_list: 209 | setattr(self, var, getattr(sat, var)) 210 | else: 211 | for var in sat.variable_list: 212 | array = getattr(sat, var) 213 | if np.ndim(array) == 1: 214 | new_array = np.append(getattr(self, var), array) 215 | setattr(self, var, new_array) 216 | elif np.ndim(array) == 2: 217 | new_array = np.append( 218 | getattr(self, var), array, axis=1) 219 | setattr(self, var, new_array) 220 | else: 221 | pass # for now ... 222 | 223 | ######################### 224 | 225 | 226 | class CygnssL2WindDisplay(object): 227 | 228 | """ 229 | This display class provides an avenue for making plots from CYGNSS L2 wind 230 | data. 231 | """ 232 | 233 | def __init__(self, cygnss_sat_object, number=4): 234 | 235 | """ 236 | cygnss_sat_object = CygnssSingle/MultiSat object, single L2 file, 237 | or list of files. 238 | number = Number of specular points to consider in RangeCorrectedGain 239 | rankings. 240 | """ 241 | # If passed string(s), try to read file(s) & make the wind data object 242 | flag = check_for_strings(cygnss_sat_object) 243 | if flag == 1: 244 | cygnss_sat_object = CygnssSingleSat(cygnss_sat_object) 245 | if flag == 2: 246 | cygnss_sat_object = CygnssMultiSat(cygnss_sat_object, 247 | number=number) 248 | if not hasattr(cygnss_sat_object, 'GoodData'): 249 | try: 250 | cygnss_sat_object.get_gain_mask(number=number) 251 | except: 252 | pass 253 | # Try again to confirm L2, this avoids problems 254 | # caused by ingest of L1 DDM 255 | if not hasattr(cygnss_sat_object, 'GoodData'): 256 | warn('Not a CYGNSS L2 wind object most likely, failing ...') 257 | return 258 | for var in cygnss_sat_object.variable_list: 259 | setattr(self, var, getattr(cygnss_sat_object, var)) 260 | if hasattr(cygnss_sat_object, 'satellites'): 261 | setattr(self, 'satellites', getattr(cygnss_sat_object, 262 | 'satellites')) 263 | self.multi_flag = True 264 | else: 265 | self.multi_flag = False 266 | self.variable_list = cygnss_sat_object.variable_list 267 | 268 | def specular_plot(self, cmap='YlOrRd', title='CYGNSS data', vmin=0, 269 | vmax=30, ms=50, marker='o', bad=-500, fig=None, ax=None, 270 | colorbar_flag=False, basemap=None, edge_flag=False, 271 | axis_label_flag=False, title_flag=True, indices=None, 272 | save=None, lonrange=None, latrange=None, 273 | truth_flag=False, return_flag=False, gpsid=None, 274 | gain=None, sat=None, **kwargs): 275 | """ 276 | Plots CYGNSS specular points on lat/lon axes using matplotlib's scatter 277 | object, which colors each point based on its wind speed value. 278 | 279 | cmap = matplotlib or user-defined colormap 280 | title = Title of plot 281 | vmin = Lowest wind speed value to display on color table 282 | vmax = Highest wind speed value to display on color table 283 | ms = Size of marker used to plot each specular point 284 | marker = Marker shape to use ('o' is best) 285 | bad = Bad value of Lat/Lon to throw out 286 | fig = matplotlib Figure object to use 287 | ax = matplotlib Axes object to use 288 | colorbar_flag = Set to True to show the colorbar 289 | basemap = Basemap object to use in plotting the specular points 290 | edge_flag = Set to True to show a black edge to make each specular 291 | point more distinctive 292 | axis_label_flag = Set to True to label lat/lon axes 293 | title_flag = Set to False to suppress title 294 | indices = Indices (2-element tuple) to use to limit the period of data 295 | shown (i.e., limit by time) 296 | save = Name of image file to save plot to 297 | lonrange = 2-element tuple to limit longitude range of plot 298 | latrange = 2-element tuple to limit latitude range of plot 299 | gpsid = Integer ID number for GPS satellite to examine 300 | sat = CYGNSS satellite number (0-7) 301 | gain = Threshold for range-corrected gain (RCG). Can be 2-ele. tuple. 302 | If so, use only RCG within that range. If scalar, then use 303 | data above the given RCG value. 304 | return_flag = Set to True to return Figure, Axes, and Basemap objects 305 | (in that order) 306 | """ 307 | ds = CygnssSubsection(self, indices=indices, bad=bad, 308 | gpsid=gpsid, gain=gain, sat=sat) 309 | if truth_flag: 310 | ws = ds.tws 311 | else: 312 | ws = ds.ws 313 | if np.size(ds.lon[ds.good]) == 0: 314 | print('No good specular points, not plotting') 315 | return 316 | fig, ax = parse_fig_ax(fig, ax) 317 | if edge_flag: 318 | ec = 'black' 319 | else: 320 | ec = 'none' 321 | if basemap is None: 322 | sc = ax.scatter(ds.lon[ds.good], ds.lat[ds.good], c=ws[ds.good], 323 | vmin=vmin, vmax=vmax, cmap=cmap, s=ms, 324 | marker=marker, edgecolors=ec, **kwargs) 325 | if lonrange is not None: 326 | ax.set_xlim(lonrange) 327 | if latrange is not None: 328 | ax.set_ylim(latrange) 329 | else: 330 | x, y = basemap(ds.lon[ds.good], ds.lat[ds.good]) 331 | sc = basemap.scatter(x, y, c=ws[ds.good], vmin=vmin, vmax=vmax, 332 | cmap=cmap, s=ms, marker=marker, edgecolors=ec, 333 | **kwargs) 334 | if colorbar_flag: 335 | plt.colorbar(sc, label='CYGNSS Wind Speed (m/s)') 336 | if axis_label_flag: 337 | plt.xlabel('Longitude (deg E)') 338 | plt.ylabel('Latitude (deg N)') 339 | if title_flag: 340 | plt.title(title) 341 | if save is not None: 342 | plt.savefig(save) 343 | if return_flag: 344 | return fig, ax, basemap, sc 345 | 346 | def histogram_plot(self, title='CYGNSS Winds vs. True Winds', fig=None, 347 | ax=None, axis_label_flag=False, title_flag=True, 348 | indices=None, bins=10, bad=-500, save=None, 349 | gain=None, sat=None): 350 | """ 351 | Plots a normalized histogram of CYGNSS wind speed vs. true wind speed 352 | (as provided by the input data to the E2ES). 353 | 354 | bins = Number of bins to use in the histogram 355 | title = Title of plot 356 | bad = Bad value of Lat/Lon to throw out 357 | fig = matplotlib Figure object to use 358 | ax = matplotlib Axes object to use 359 | axis_label_flag = Set to True to label lat/lon axes 360 | title_flag = Set to False to suppress title 361 | indices = Indices (2-element tuple) to use to limit the period of data 362 | shown (i.e., limit by time) 363 | gain = Threshold for range-corrected gain (RCG). Can be 2-ele. tuple. 364 | If so, use only RCG within that range. If scalar, then use 365 | data above the given RCG value. 366 | save = Name of image file to save plot to 367 | sat = CYGNSS satellite number (0-7) 368 | """ 369 | ds = CygnssSubsection(self, indices=indices, gain=gain, bad=bad, 370 | sat=sat) 371 | if np.size(ds.lon[ds.good]) == 0: 372 | print('No good specular points, not plotting') 373 | return 374 | fig, ax = parse_fig_ax(fig, ax) 375 | ax.hist(ds.ws[ds.good].ravel()-ds.tws[ds.good].ravel(), bins=bins, 376 | normed=True) 377 | if axis_label_flag: 378 | plt.xlabel('CYGNSS Wind Speed - True Wind Speed (m/s)') 379 | plt.ylabel('Frequency') 380 | if title_flag: 381 | plt.title(title) 382 | if save is not None: 383 | plt.savefig(save) 384 | 385 | def hist2d_plot(self, title='CYGNSS Winds vs. True Winds', fig=None, 386 | ax=None, axis_label_flag=False, title_flag=True, 387 | indices=None, bins=20, bad=-500, save=None, 388 | gain=None, colorbar_flag=True, 389 | cmap='YlOrRd', range=(0, 20), ls='--', 390 | add_line=True, line_color='r', sat=None, 391 | colorbar_label_flag=True, **kwargs): 392 | """ 393 | Plots a normalized 2D histogram of CYGNSS wind speed vs. true wind spd 394 | (as provided by the input data to the E2ES). This information can be 395 | thresholded by RangeCorrectedGain 396 | 397 | bins = Number of bins to use in the histogram 398 | title = Title of plot 399 | bad = Bad value of Lat/Lon to throw out 400 | fig = matplotlib Figure object to use 401 | ax = matplotlib Axes object to use 402 | axis_label_flag = Set to True to label lat/lon axes 403 | title_flag = Set to False to suppress title 404 | indices = Indices (2-element tuple) to use to limit the period of data 405 | shown (i.e., limit by time) 406 | gain = Threshold for range-corrected gain (RCG). Can be 2-ele. tuple. 407 | If so, use only RCG within that range. If scalar, then use 408 | data above the given RCG value. 409 | save = Name of image file to save plot to 410 | sat = CYGNSS satellite number (0-7) 411 | **kwargs = Whatever else pyplot.hist2d will accept 412 | """ 413 | ds = CygnssSubsection(self, indices=indices, gain=gain, 414 | bad=bad, sat=sat) 415 | if np.size(ds.lon[ds.good]) == 0: 416 | print('No good specular points, not plotting') 417 | return 418 | fig, ax = parse_fig_ax(fig, ax) 419 | H, xedges, yedges, img = ax.hist2d( 420 | ds.ws[ds.good].ravel(), ds.tws[ds.good].ravel(), bins=bins, 421 | normed=True, cmap=cmap, zorder=1, range=[range, range], 422 | **kwargs) 423 | # These 2 lines are a hack to get color bar, but not plot anything new 424 | extent = [xedges[0], xedges[-1], yedges[0], yedges[-1]] 425 | im = ax.imshow(H.T, cmap=cmap, extent=extent, zorder=0) 426 | ax.set_xlim(range) 427 | ax.set_ylim(range) 428 | if add_line: 429 | ax.plot(range, range, ls=ls, 430 | color=line_color, lw=2, zorder=2) 431 | if axis_label_flag: 432 | ax.set_xlabel('CYGNSS Wind Speed (m/s)') 433 | ax.set_ylabel('True Wind Speed (m/s)') 434 | if title_flag: 435 | ax.set_title(title) 436 | if colorbar_flag: 437 | if colorbar_label_flag: 438 | label = 'Frequency' 439 | else: 440 | label = '' 441 | plt.colorbar(im, label=label, ax=ax, shrink=0.75) 442 | if save is not None: 443 | plt.savefig(save) 444 | 445 | ######################### 446 | 447 | 448 | class CygnssSubsection(object): 449 | 450 | """ 451 | Class to handle subsectioning CYGNSS data. Subsectioning by 452 | satellite (via CygnssSingleSat input), time indices, GPS satellite ID, 453 | range-corrected gain, etc. is supported. 454 | 455 | Main Attributes 456 | --------------- 457 | ws = Wind speed array 458 | lon = Longitude array 459 | lat = Latitude array 460 | gd = GoodData array 461 | rcg = RangeCorrectedGain array 462 | gps = GpsID array 463 | """ 464 | 465 | def __init__(self, data, indices=None, gpsid=None, gain=None, bad=-500, 466 | sat=None): 467 | """ 468 | data = CygnssSingleSat, CygnssMultiSat, or CygnssL2WindDisplay object 469 | gpsid = Integer ID number for GPS satellite to examine 470 | gain = Threshold by range-corrected gain, values below will be masked 471 | bad = Value to compare against lat/lon to mask out missing data 472 | sat = CYGNSS satellite number (0-7) 473 | indices = Indices (2-element tuple) to use to limit the period of data 474 | shown (i.e., limit by time) 475 | """ 476 | # Set basic attributes based on input data object 477 | if sat is not None and hasattr(data, 'satellites'): 478 | data = data.satellites[sat] 479 | self.ws = data.WindSpeed 480 | self.tws = data.TruthWindSpeed 481 | self.lon = data.Longitude 482 | self.lat = data.Latitude 483 | self.gps = data.GpsID 484 | self.rcg = data.RangeCorrectedGain 485 | self.gd = data.GoodData 486 | 487 | # Set keyword-based attributes 488 | self.gpsid = gpsid 489 | self.gain = gain 490 | self.bad = bad 491 | self.indices = indices 492 | 493 | # Now subsection the data 494 | self.subsection_data() 495 | self.get_good_data_mask() 496 | 497 | def subsection_data(self): 498 | """ 499 | This method subsections the L2 wind data and returns these as arrays 500 | ready to plot. 501 | """ 502 | if self.indices is not None: 503 | self.ws = self.ws[self.indices[0]:self.indices[1]][:] 504 | self.tws = self.tws[self.indices[0]:self.indices[1]][:] 505 | self.lon = self.lon[self.indices[0]:self.indices[1]][:] 506 | self.lat = self.lat[self.indices[0]:self.indices[1]][:] 507 | self.gd = self.gd[self.indices[0]:self.indices[1]][:] 508 | self.gps = self.gps[self.indices[0]:self.indices[1]][:] 509 | self.rcg = self.rcg[self.indices[0]:self.indices[1]][:] 510 | 511 | def get_good_data_mask(self): 512 | """ 513 | Sets a mask used to limit the data plotted. Filtered out are data 514 | masked out by the GoodData mask (based on RangeCorrectedGain), missing 515 | lat/lon values, and bad data (ws < 0) 516 | """ 517 | good1 = np.logical_and(self.gd == 1, self.ws >= 0) 518 | good2 = np.logical_and(self.lon > self.bad, self.lat > self.bad) 519 | if self.gpsid is not None and type(self.gpsid) is int: 520 | good2 = np.logical_and(good2, self.gps == self.gpsid) 521 | if self.gain is not None: 522 | if np.size(self.gain) == 2: 523 | cond = np.logical_and(self.rcg >= self.gain[0], 524 | self.rcg < self.gain[1]) 525 | good2 = np.logical_and(good2, cond) 526 | else: 527 | good2 = np.logical_and(good2, self.rcg >= self.gain) 528 | self.good = np.logical_and(good1, good2) 529 | 530 | ######################### 531 | 532 | 533 | class CygnssTrack(object): 534 | 535 | """ 536 | Class to facilitate extraction of a single track of specular points 537 | from a CygnssSingleSat, CygnssMultiSat, or CygnssL2WindDisplay object. 538 | 539 | Attributes 540 | ---------- 541 | input = CygnssSubsection object 542 | ws = CYGNSS wind speeds 543 | tws = Truth wind speeds 544 | lon = Longitudes of specular points 545 | lat = Latitudes of specular points 546 | rcg = Range-corrected gains of specular points 547 | datetimes = Datetime objects for specular points 548 | 549 | The following attributes are created by filter_track method: 550 | fws = Filtered wind speeds 551 | flon = Filtered longitudes 552 | flat = Filtered latitudes 553 | These attributes are shorter than the main attributes by the window length 554 | """ 555 | 556 | def __init__(self, data, datetimes=None, **kwargs): 557 | """ 558 | data = CygnssSingleSat, CygnssMultiSat, or CygnssL2WindDisplay object 559 | datetimes = List of datetime objects from get_datetime function. 560 | If None, this function is called. 561 | """ 562 | self.input = CygnssSubsection(data, **kwargs) 563 | self.ws = self.input.ws[self.input.good] 564 | self.tws = self.input.tws[self.input.good] 565 | self.lon = self.input.lon[self.input.good] 566 | self.lat = self.input.lat[self.input.good] 567 | self.rcg = self.input.rcg[self.input.good] 568 | if datetimes is None: 569 | dts = get_datetime(data) 570 | else: 571 | dts = datetimes 572 | if self.input.indices is not None: 573 | self.datetimes = dts[ 574 | self.input.indices[0]:self.input.indices[1]][self.input.good] 575 | else: 576 | self.datetimes = dts[self.input.good] 577 | 578 | def filter_track(self, window=5): 579 | """ 580 | Applies a running-mean filter to the track. 581 | 582 | window = Number of specular points in the running mean window. 583 | Must be odd. 584 | """ 585 | if window % 2 == 0: 586 | raise ValueError('Window must be odd length, not even.') 587 | hl = int((window - 1) / 2) 588 | self.fws = np.convolve( 589 | self.ws, np.ones((window,))/window, mode='valid') 590 | self.flon = self.lon[hl:-1*hl] 591 | self.flat = self.lat[hl:-1*hl] 592 | 593 | ######################### 594 | 595 | 596 | class E2esInputData(NetcdfFile): 597 | 598 | """Base class for ingesting E2ES input data. Child class of NetcdfFile.""" 599 | 600 | def get_wind_speed(self): 601 | """ 602 | Input E2ES data normally don't have wind speed as a field. This method 603 | fixes that. 604 | """ 605 | self.WindSpeed = np.sqrt(self.eastward_wind**2 + 606 | self.northward_wind**2) 607 | self.variable_list.append('WindSpeed') 608 | 609 | ######################### 610 | 611 | 612 | class InputWindDisplay(object): 613 | 614 | """Display object for the E2ES input data""" 615 | 616 | def __init__(self, input_winds_object): 617 | """ 618 | input_winds_object = Input E2esInputData object or wind file 619 | """ 620 | # If passed a string, try to read the file & make input data object 621 | if isinstance(input_winds_object, string_types): 622 | input_winds_object = E2esInputData(input_winds_object) 623 | if not hasattr(input_winds_object, 'WindSpeed'): 624 | input_winds_object.get_wind_speed() 625 | for var in input_winds_object.variable_list: 626 | setattr(self, var, getattr(input_winds_object, var)) 627 | self.make_coordinates_2d() 628 | 629 | def basemap_plot(self, fill_color='#ACACBF', ax=None, fig=None, 630 | time_index=0, cmap='YlOrRd', vmin=0, vmax=30, 631 | colorbar_flag=True, return_flag=True, save=None, 632 | title='Input Wind Speed', title_flag=True, 633 | show_grid=False): 634 | """ 635 | Plots E2ES input wind speed data on a Basemap using matplotlib's 636 | pcolormesh object. Defaults to return the Basemap object so other 637 | things (e.g., CYGNSS data) can be overplotted. 638 | 639 | fill_color = Color to fill continents 640 | time_index = If the input data contain more than one time step, this 641 | index selects the time step to display 642 | cmap = matplotlib or user-defined colormap 643 | title = Title of plot 644 | vmin = Lowest wind speed value to display on color table 645 | vmax = Highest wind speed value to display on color table 646 | fig = matplotlib Figure object to use 647 | ax = matplotlib Axes object to use 648 | return_flag = Set to False to suppress Basemap object return 649 | title_flag = Set to False to suppress title 650 | save = Name of image file to save plot to 651 | colorbar_flag = Set to False to suppress the colorbar 652 | show_grid = Set to True to show the lat/lon grid and label it 653 | """ 654 | fig, ax = parse_fig_ax(fig, ax) 655 | m = get_basemap(lonrange=[np.min(self.longitude), 656 | np.max(self.longitude)], 657 | latrange=[np.min(self.latitude), 658 | np.max(self.latitude)]) 659 | m.fillcontinents(color=fill_color) 660 | x, y = m(self.longitude, self.latitude) 661 | cs = m.pcolormesh(x, y, self.WindSpeed[time_index], 662 | vmin=vmin, vmax=vmax, cmap=cmap) 663 | if show_grid: 664 | m.drawmeridians( 665 | np.arange(-180, 180, 5), labels=[True, True, True, True]) 666 | m.drawparallels( 667 | np.arange(-90, 90, 5), labels=[True, True, True, True]) 668 | if title_flag: 669 | plt.title(title) 670 | if colorbar_flag: 671 | m.colorbar(cs, label='Wind Speed (m/s)', location='bottom', 672 | pad="7%") 673 | if save is not None: 674 | plt.savefig(save) 675 | if return_flag: 676 | return m 677 | 678 | def make_coordinates_2d(self): 679 | if np.ndim(self.longitude) == 1: 680 | lon2d, lat2d = np.meshgrid(self.longitude, self.latitude) 681 | self.longitude = lon2d 682 | self.latitude = lat2d 683 | 684 | ############################## 685 | # Independent Functions Follow 686 | ############################## 687 | 688 | 689 | def get_tracks(data, indices=None, min_samples=10, verbose=False, 690 | filter=False, window=5): 691 | """ 692 | Returns a list of CygnssTrack objects from a CYGNSS data or display object 693 | 694 | data = CygnssSingleSat, CygnssMultiSat, or CygnssL2WindDisplay object 695 | indices = Indices (2-element tuple) to use to limit the period of data 696 | shown (i.e., limit by time). Not usually necessary unless 697 | processing more than one day's worth of data. 698 | min_samples = Minimum allowable track size (number of specular points) 699 | verbose = Set to True for some text updates while running 700 | filter = Set to True to filter each track 701 | window = Window length of filter, in # of specular points. Must be odd. 702 | """ 703 | trl = [] 704 | dts = get_datetime(data) 705 | # For some reason range works but np.arange doesn't. 706 | for csat in range(8): 707 | if not hasattr(data, 'satellites'): 708 | if csat > 0: 709 | break 710 | if verbose: 711 | print('CYGNSS satellite', csat) 712 | for gsat in range(np.max(data.GpsID)+1): 713 | # This will isolate most tracks, improving later cluster analysis 714 | ds = CygnssTrack(data, datetimes=dts, indices=indices, gpsid=gsat, 715 | sat=csat) 716 | if np.size(ds.lon) > 0: 717 | # Cluster analysis separates out any remaining grouped tracks 718 | X = list(zip(ds.lon, ds.lat)) 719 | db = DBSCAN(min_samples=min_samples).fit(X) 720 | labels = db.labels_ 721 | uniq = np.unique(labels) 722 | for element in uniq[uniq >= 0]: 723 | # A bit clunky, but make a copy of the CygnssTrack object 724 | # to help separate out remaining tracks in the scene 725 | dsc = deepcopy(ds) 726 | dsc.lon = ds.lon[labels == element] 727 | dsc.lat = ds.lat[labels == element] 728 | dsc.ws = ds.ws[labels == element] 729 | dsc.tws = ds.tws[labels == element] 730 | dsc.rcg = ds.lon[labels == element] 731 | dsc.datetimes = ds.datetimes[labels == element] 732 | dsc.sat = csat 733 | dsc.prn = gsat 734 | trl.append(dsc) 735 | if filter: 736 | for tr in trl: 737 | tr.filter_track(window=window) 738 | return trl 739 | 740 | 741 | def get_datetime(data): 742 | if hasattr(data, 'satellites'): 743 | data = data.satellites[0] 744 | dts = [] 745 | for i in np.arange(len(data.Year)): 746 | dti = dt.datetime(data.Year[i], data.Month[i], data.Day[i], 747 | data.Hour[i], data.Minute[i], data.Second[i]) 748 | tmplist = [dti for i in np.arange(15)] 749 | dts.append(tmplist) 750 | return np.array(dts) 751 | 752 | 753 | def parse_fig_ax(fig, ax): 754 | """ 755 | Parse matplotlib Figure and Axes objects, if provided, or just grab the 756 | current ones in memory. 757 | """ 758 | if fig is None: 759 | fig = plt.gcf() 760 | if ax is None: 761 | ax = plt.gca() 762 | return fig, ax 763 | 764 | 765 | def get_basemap(latrange=[-90, 90], lonrange=[-180, 180], resolution='l', 766 | area_thresh=1000): 767 | """ 768 | Function to create a specifically formatted Basemap provided the input 769 | parameters. 770 | 771 | latrange = Latitude range of the plot (2-element tuple) 772 | lonrange = Longitude range of the plot (2-element tuple) 773 | resolution = Resolution of the Basemap 774 | area_thresh = Threshold (in km^**2) for displaying small features, such as 775 | lakes/islands 776 | """ 777 | lon_0 = np.mean(lonrange) 778 | lat_0 = np.mean(latrange) 779 | m = Basemap(projection='merc', lon_0=lon_0, lat_0=lat_0, lat_ts=lat_0, 780 | llcrnrlat=np.min(latrange), urcrnrlat=np.max(latrange), 781 | llcrnrlon=np.min(lonrange), urcrnrlon=np.max(lonrange), 782 | rsphere=6371200., resolution=resolution, 783 | area_thresh=area_thresh) 784 | m.drawcoastlines() 785 | m.drawstates() 786 | m.drawcountries() 787 | return m 788 | 789 | 790 | def check_for_strings(var): 791 | """ 792 | Given an input var, check to see if it is a string (scalar or array of 793 | strings), or something else. 794 | 795 | Output: 796 | 0 = non-string, 1 = string scalar, 2 = string array 797 | """ 798 | if np.size(var) == 1: 799 | if isinstance(var, string_types): 800 | return 1 801 | else: 802 | return 0 803 | else: 804 | for val in var: 805 | if not isinstance(val, string_types): 806 | return 0 807 | return 2 808 | -------------------------------------------------------------------------------- /build/lib/pygnss/orbit.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import datetime as dt 3 | from copy import deepcopy 4 | import xarray 5 | from sklearn.cluster import DBSCAN 6 | # from scipy.signal import convolve 7 | import h5py 8 | import scipy 9 | import os 10 | from memory_profiler import profile 11 | 12 | array_list = ['lon', 'lat', 'ws', 'rcg', 'ws_yslf_nbrcs', 13 | 'ws_yslf_les', 'datetimes', 'sod'] 14 | scalar_list = ['antenna', 'prn', 'sat'] 15 | 16 | 17 | def read_cygnss_l2(fname): 18 | return xarray.open_dataset(fname) 19 | 20 | 21 | def read_imerg(fname): 22 | return Imerg(fname) 23 | 24 | 25 | def split_tracks_in_time(track, gap=60): 26 | """ 27 | This function will split a CygnssTrack object into two separate tracks 28 | if there is a significant gap in time. Currently can split a track up to 29 | three times. 30 | 31 | Parameters 32 | ---------- 33 | track : CygnssTrack object 34 | CYGNSS track that needs to be checked for breaks in time 35 | 36 | Other Parameters 37 | ---------------- 38 | gap : int 39 | Number of seconds in a gap before a split is forced 40 | 41 | Returns 42 | ------- 43 | track_list : list 44 | List of CygnssTrack objects broken up from original track 45 | """ 46 | indices = np.where(np.diff(track.sod) > gap)[0] 47 | if len(indices) > 0: 48 | if len(indices) == 1: 49 | track1 = subset_track(deepcopy(track), 0, indices[0]+1) 50 | track2 = subset_track(deepcopy(track), indices[0]+1, 51 | len(track.sod)) 52 | return [track1, track2] 53 | if len(indices) == 2: 54 | track1 = subset_track(deepcopy(track), 0, indices[0]+1) 55 | track2 = subset_track(deepcopy(track), indices[0]+1, 56 | indices[1]+1) 57 | track3 = subset_track(deepcopy(track), indices[1]+1, 58 | len(track.sod)) 59 | return [track1, track2, track3] 60 | if len(indices) == 3: 61 | track1 = subset_track(deepcopy(track), 0, indices[0]+1) 62 | track2 = subset_track(deepcopy(track), indices[0]+1, indices[1]+1) 63 | track3 = subset_track(deepcopy(track), indices[1]+1, indices[2]+1) 64 | track4 = subset_track(deepcopy(track), indices[2]+1, 65 | len(track.sod)) 66 | return [track1, track2, track3, track4] 67 | else: 68 | print('Found more than four tracks!') 69 | return 0 70 | 71 | 72 | def subset_track(track, index1, index2): 73 | """ 74 | This function subsets a CYGNSS track to only include data from a range 75 | defined by two indexes. 76 | 77 | Parameters 78 | ---------- 79 | track : CygnssTrack object 80 | CygnssTrack to be subsetted 81 | index1 : int 82 | Starting index 83 | index2 : int 84 | Ending index 85 | 86 | Returns 87 | ------- 88 | track : CygnssTrack object 89 | Subsetted CygnssTrack object 90 | """ 91 | for arr in array_list: 92 | setattr(track, arr, getattr(track, arr)[index1:index2]) 93 | for scalar in scalar_list: 94 | setattr(track, scalar, getattr(track, scalar)) 95 | return track 96 | 97 | 98 | def get_tracks(data, sat, min_samples=10, verbose=False, 99 | filter=False, window=5, eps=1, gap=60): 100 | """ 101 | Returns a list of isolated CygnssTrack objects from a CYGNSS data object. 102 | 103 | Parameters 104 | ---------- 105 | data : xarray.core.dataset.Dataset object 106 | CYGNSS data object as read by xarray.open_dataset 107 | sat : int 108 | CYGNSS satellite to be analyzed. 109 | 110 | Other Parameters 111 | ---------------- 112 | min_samples : int 113 | Minimum allowable track size (number of specular points) 114 | verbose : bool 115 | True - Provide text updates while running 116 | 117 | False - Don't do this 118 | 119 | filter : bool 120 | True - Each track will receive a filter 121 | 122 | False - Don't do this 123 | 124 | window : int 125 | Window length of filter, in number of specular points. Must be odd. 126 | eps : scalar 127 | This is the eps keyword to be passed to DBSCAN. It is the max distance 128 | (in degrees lat/lon) between two tracks for them to be considered as 129 | part of the same track. 130 | gap : int 131 | Number of seconds in a track gap before a split is forced 132 | 133 | Returns 134 | ------- 135 | trl : list 136 | List of isolated CygnssTrack objects 137 | """ 138 | trl = [] 139 | dts = get_datetime(data) 140 | # Currently only works for one satellite at a time due to resource issues 141 | if type(sat) is not int or sat < 1 or sat > 8: 142 | raise ValueError('sat must be integer between 1 and 8') 143 | else: 144 | csat = sat 145 | if verbose: 146 | print('CYGNSS satellite', csat) 147 | print('GPS code (max =', str(int(np.max(data.prn_code.data)))+'):', 148 | end=' ') 149 | for gsat in range(np.int16(np.max(data.prn_code.data)+1)): 150 | if verbose: 151 | print(gsat, end=' ') 152 | # This will isolate most tracks, improving later cluster analysis 153 | for ant in range(np.int16(np.max(data.antenna.data)+1)): 154 | ds = CygnssTrack(data, datetimes=dts, gpsid=gsat, 155 | sat=csat, antenna=ant) 156 | if np.size(ds.lon) > 0: 157 | # Cluster analysis separates out additional grouped tracks 158 | # Only simplistic analysis of lat/lon gaps in degrees needed 159 | X = list(zip(ds.lon, ds.lat)) 160 | db = DBSCAN(min_samples=min_samples, eps=eps).fit(X) 161 | labels = db.labels_ 162 | uniq = np.unique(labels) 163 | for element in uniq[uniq >= 0]: 164 | # A bit clunky, but make a copy of the CygnssTrack object 165 | # to help separate out remaining tracks in the scene 166 | dsc = deepcopy(ds) 167 | for key in array_list: 168 | setattr(dsc, key, getattr(ds, key)[labels == element]) 169 | dsc.lon[dsc.lon > 180] -= 360.0 170 | for key in scalar_list: 171 | setattr(dsc, key, np.array(getattr(ds, key))[0]) 172 | # Final separation by splitting about major time gaps 173 | test = split_tracks_in_time(dsc, gap=gap) 174 | if test is None: # No time gap, append the original track 175 | trl.append(dsc) 176 | # Failsafe - Ignore difficult-to-split combined tracks 177 | elif test == 0: 178 | pass 179 | else: # Loop thru split-up tracks and append separately 180 | for t in test: 181 | trl.append(t) 182 | del dsc 183 | del db, labels, uniq, X 184 | del ds # This function is a resource hog, forcing some cleanup 185 | if filter: 186 | for tr in trl: 187 | tr.filter_track(window=window) 188 | return trl 189 | 190 | 191 | def get_datetime(cyg): 192 | epoch_start = np.datetime64('1970-01-01T00:00:00Z') 193 | tdelta = np.timedelta64(1, 's') 194 | return np.array([dt.datetime.utcfromtimestamp((st - epoch_start) / tdelta) 195 | for st in cyg.sample_time.data]) 196 | 197 | 198 | class CygnssSubsection(object): 199 | 200 | """ 201 | Class to handle subsectioning CYGNSS data. Subsectioning by 202 | satellite (via CygnssSingleSat input), time indices, GPS satellite ID, 203 | range-corrected gain, etc. is supported. 204 | 205 | Main Attributes 206 | --------------- 207 | ws = Wind speed array 208 | lon = Longitude array 209 | lat = Latitude array 210 | rcg = RangeCorrectedGain array 211 | gps = GpsID array 212 | """ 213 | 214 | def __init__(self, data, gpsid=None, gain=None, sat=None, antenna=None): 215 | """ 216 | data = CygnssSingleSat, CygnssMultiSat, or CygnssL2WindDisplay object 217 | gpsid = Integer ID number for GPS satellite to examine 218 | gain = Threshold by range-corrected gain, values below will be masked 219 | bad = Value to compare against lat/lon to mask out missing data 220 | sat = CYGNSS satellite number (1-8) 221 | """ 222 | # Set basic attributes based on input data object 223 | self.ws = data.wind_speed.data 224 | self.ws_yslf_nbrcs = data.yslf_nbrcs_wind_speed.data 225 | self.ws_yslf_les = data.yslf_les_wind_speed.data 226 | self.lon = data.lon.data 227 | self.lat = data.lat.data 228 | self.gps = np.int16(data.prn_code.data) 229 | self.antenna = np.int16(data.antenna.data) 230 | self.rcg = data.range_corr_gain.data 231 | self.cygnum = np.int16(data.spacecraft_num.data) 232 | 233 | # Set keyword-based attributes 234 | self.gpsid = gpsid 235 | self.gain = gain 236 | self.sat = sat 237 | self.ant_num = antenna 238 | 239 | # Now subsection the data 240 | self.get_good_data_mask() 241 | 242 | def get_good_data_mask(self): 243 | """ 244 | Sets a mask used to limit the data plotted. Filtered out are data 245 | masked out by the GoodData mask (based on RangeCorrectedGain), missing 246 | lat/lon values, and bad data (ws < 0) 247 | """ 248 | good1 = self.ws >= 0 249 | good2 = np.logical_and(np.isfinite(self.lon), np.isfinite(self.lat)) 250 | if self.gpsid is not None and type(self.gpsid) is int: 251 | good2 = np.logical_and(good2, self.gps == self.gpsid) 252 | if self.gain is not None: 253 | if np.size(self.gain) == 2: 254 | cond = np.logical_and(self.rcg >= self.gain[0], 255 | self.rcg < self.gain[1]) 256 | good2 = np.logical_and(good2, cond) 257 | else: 258 | good2 = np.logical_and(good2, self.rcg >= self.gain) 259 | if self.sat is not None and type(self.sat) is int: 260 | good2 = np.logical_and(good2, self.cygnum == self.sat) 261 | if self.ant_num is not None and type(self.sat) is int: 262 | good2 = np.logical_and(good2, self.antenna == self.ant_num) 263 | self.good = np.logical_and(good1, good2) 264 | 265 | 266 | class CygnssTrack(object): 267 | 268 | """ 269 | Class to facilitate extraction of a single track of specular points 270 | from a CygnssSingleSat, CygnssMultiSat, or CygnssL2WindDisplay object. 271 | 272 | Attributes 273 | ---------- 274 | input = CygnssSubsection object 275 | ws = CYGNSS wind speeds 276 | lon = Longitudes of specular points 277 | lat = Latitudes of specular points 278 | rcg = Range-corrected gains of specular points 279 | datetimes = Datetime objects for specular points 280 | 281 | The following attributes are created by filter_track method: 282 | fws = Filtered wind speeds 283 | flon = Filtered longitudes 284 | flat = Filtered latitudes 285 | These attributes are shorter than the main attributes by the window length 286 | """ 287 | 288 | def __init__(self, data, datetimes=None, **kwargs): 289 | """ 290 | data = CygnssSingleSat, CygnssMultiSat, or CygnssL2WindDisplay object 291 | datetimes = List of datetime objects from get_datetime function. 292 | If None, this function is called. 293 | """ 294 | self.input = CygnssSubsection(data, **kwargs) 295 | self.ws = self.input.ws[self.input.good] 296 | self.ws_yslf_nbrcs = self.input.ws_yslf_nbrcs[self.input.good] 297 | self.ws_yslf_les = self.input.ws_yslf_les[self.input.good] 298 | self.lon = self.input.lon[self.input.good] 299 | self.lat = self.input.lat[self.input.good] 300 | self.rcg = self.input.rcg[self.input.good] 301 | self.antenna = self.input.antenna[self.input.good] 302 | self.prn = self.input.gps[self.input.good] 303 | self.sat = self.input.cygnum[self.input.good] 304 | if datetimes is None: 305 | dts = get_datetime(data) 306 | else: 307 | dts = datetimes 308 | self.datetimes = dts[self.input.good] 309 | sod = [] 310 | for dt1 in self.datetimes: 311 | sod.append((dt1 - dt.datetime( 312 | self.datetimes[0].year, self.datetimes[0].month, 313 | self.datetimes[0].day)).total_seconds()) 314 | self.sod = np.array(sod) 315 | 316 | def filter_track(self, window=5): 317 | """ 318 | Applies a running-mean filter to the track. 319 | 320 | window = Number of specular points in the running mean window. 321 | Must be odd. 322 | """ 323 | if window % 2 == 0: 324 | raise ValueError('Window must be odd length, not even.') 325 | hl = int((window - 1) / 2) 326 | self.fws = np.convolve( 327 | self.ws, np.ones((window,))/window, mode='valid') 328 | self.flon = self.lon[hl:-1*hl] 329 | self.flat = self.lat[hl:-1*hl] 330 | 331 | 332 | class Imerg(object): 333 | 334 | def __init__(self, filen): 335 | self.read_imerg(filen) 336 | 337 | def read_imerg(self, filen): 338 | imerg = h5py.File(filen, 'r') 339 | self.datetime = dt.datetime.strptime(os.path.basename(filen)[23:39], 340 | '%Y%m%d-S%H%M%S') 341 | self.precip = np.ma.masked_where( 342 | np.transpose(imerg['Grid']['precipitationCal']) <= 0, 343 | np.transpose(imerg['Grid']['precipitationCal'])) 344 | self.lon = np.array(imerg['Grid']['lon']) 345 | self.lat = np.array(imerg['Grid']['lat']) 346 | self.filename = os.path.basename(filen) 347 | imerg.close() 348 | 349 | def downsample(self): 350 | filled_precip = self.precip.filled(fill_value=0.0) 351 | dummy = scipy.ndimage.interpolation.zoom(filled_precip, 0.5) 352 | self.coarse_precip = np.ma.masked_where(dummy <= 0, dummy) 353 | self.coarse_lon = self.lon[::2] 354 | self.coarse_lat = self.lat[::2] 355 | 356 | 357 | def add_imerg(trl, ifiles, dt_imerg): 358 | for ii in range(len(trl)): 359 | check_dt = trl[ii].datetimes[len(trl[ii].sod)//2] 360 | # diff = np.abs(check_dt - dt_imerg) 361 | index = np.where(dt_imerg <= check_dt)[0][-1] # np.argmin(diff) 362 | imerg = Imerg(ifiles[index]) 363 | if ii % 50 == 0: 364 | print(ii, end=' ') 365 | precip = [] 366 | for j in range(len(trl[ii].lon)): 367 | ilon = int(np.round((trl[ii].lon[j] - imerg.lon[0]) / 0.10)) 368 | ilat = int(np.round((trl[ii].lat[j] - imerg.lat[0]) / 0.10)) 369 | precip.append(imerg.precip[ilat, ilon]) 370 | precip = np.array(precip) 371 | precip[~np.isfinite(precip)] = 0.0 372 | setattr(trl[ii], 'precip', precip) 373 | setattr(trl[ii], 'imerg', os.path.basename(ifiles[index])) 374 | print() 375 | return trl 376 | 377 | 378 | def write_netcdfs(trl, path): 379 | for i, track in enumerate(trl): 380 | fname = 'track_' + str(track.sat).zfill(2) + '_' + \ 381 | str(track.prn).zfill(2) + \ 382 | '_' + str(track.antenna).zfill(2) + '_' + str(i).zfill(4) + \ 383 | track.datetimes[0].strftime('_%Y%m%d_s%H%M%S_') + \ 384 | track.datetimes[-1].strftime('e%H%M%S.nc') 385 | ds = xarray.Dataset( 386 | {'ws': (['nt'], track.ws), 387 | 'ws_yslf_nbrcs': (['nt'], track.ws_yslf_nbrcs), 388 | 'ws_yslf_les': (['nt'], track.ws_yslf_les), 389 | 'lat': (['nt'], track.lat), 390 | 'lon': (['nt'], track.lon), 391 | 'datetimes': (['nt'], track.datetimes), 392 | 'rcg': (['nt'], track.rcg), 393 | 'precip': (['nt'], track.precip), 394 | 'sod': (['nt'], track.sod)}, 395 | coords={'nt': (['nt'], np.arange(len(track.ws)))}, 396 | attrs={'imerg': track.imerg}) 397 | ds.to_netcdf(path + fname, format='NETCDF3_CLASSIC') 398 | ds.close() 399 | del(ds) 400 | -------------------------------------------------------------------------------- /dist/PyGNSS-0.7-py3.6.egg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasa/PyGNSS/fae99c4fec1073a72dc1bfcd63da38a7376c0e67/dist/PyGNSS-0.7-py3.6.egg -------------------------------------------------------------------------------- /pygnss/__init__.py: -------------------------------------------------------------------------------- 1 | from . import e2es 2 | from . import orbit 3 | -------------------------------------------------------------------------------- /pygnss/e2es.py: -------------------------------------------------------------------------------- 1 | """ 2 | Title/Version 3 | ------------- 4 | Python CYGNSS Toolkit (PyGNSS) 5 | pygnss v0.7 6 | Developed & tested with Python 2.7 and 3.4 7 | 8 | 9 | Author 10 | ------ 11 | Timothy Lang 12 | NASA MSFC 13 | timothy.j.lang@nasa.gov 14 | (256) 961-7861 15 | 16 | 17 | Overview 18 | -------- 19 | This module enables the ingest, analysis, and plotting of Cyclone Global 20 | Navigation Satellite System (CYGNSS) End-to-End Simulator (E2ES) input and 21 | output data. To use, place in PYTHONPATH and use the following import command: 22 | import pygnss 23 | 24 | 25 | Notes 26 | ----- 27 | Requires - numpy, matplotlib, Basemap, netCDF4, warnings, os, six, datetime, 28 | sklearn, copy 29 | 30 | 31 | Change Log 32 | ---------- 33 | v0.7 Major Changes (04/20/2016) 34 | 1. Added CygnssSubsection class to consolidate and simplify data subsectioning. 35 | Can be called independently, but is also used by the plotting methods in 36 | CygnssL2WindDisplay class. Moved the subsection_data and get_good_data_mask 37 | methods to this new class, and greatly modifed them to eliminate 38 | method returns. 39 | 2. Added ability to subsection data by CYGNSS and GPS satellite numbers, as 40 | well as by range-corrected gain threshold or interval. All plotting routines 41 | now support these subsectioning capabilities. 42 | 3. Added get_datetime indpendent function to derive datetime objects in the 43 | same array shape as the WindSpeed data. Added datetime module dependency. 44 | 4. Added CygnssTrack class to leverage CygnssSubsection to help isolate 45 | and contain all the data from an individual track. Also enables filtering 46 | of wind speeds along a track. 47 | 5. Added get_tracks independent function to isolate all individual tracks, and 48 | return a list of CygnssTrack objects from a CygnssSingleSat, CygnssMultiSat, 49 | or CygnssL2WindDisplay object. 50 | 51 | v0.6 Major Changes (11/20/2015) 52 | 1. Added hist2d_plot method to CygnssL2WindDisplay. 53 | 2. Added threshold keyword to allow filtering of histrogram figures by 54 | RangeCorrectedGain windows. 55 | 56 | v0.5 Major Changes (08/10/2015) 57 | 1. Supports Python 3 now. 58 | 59 | v0.4 Major Changes (07/02/2015) 60 | 1. Made all code pep8 compliant. 61 | 62 | v0.3 Major Changes (03/19/2015) 63 | 1. Documentation improvements. Doing help(pygnss) should be more useful now. 64 | 2. Fixed bug where GoodData attribute was not getting set for CygnssSingleSat 65 | objects after they were input into CygnssL2WindDisplay. 66 | 3. Fixes to ensure CygnssSingle/MultiSat classes can ingest L1 DDM files w/out 67 | errors. This provides a basis for adding L1 DDM functionality to PyGNSS. 68 | 4. Swapped out np.rank for np.ndim due to annoying deprecation warnings. 69 | 70 | v0.2 Major Changes (02/24/2015) 71 | 1. Fixed miscellaneous bugs related to data subsectioning and plotting. 72 | 2. Added histogram plot for CYGNSS vs. Truth winds. 73 | 74 | v0.1 Functionality: 75 | 1. Reads netCDFs for input to CYGNSS E2ES as well as output files. 76 | 2. Can ingest single-satellite data or merge all 8 together. 77 | 3. Capable of masking L2 wind data by RangeCorrectedGain. 78 | 4. Basic display objects & plotting routines exist for input/output data, w/ 79 | support for combined input/output plots. 80 | 81 | 82 | Planned Updates 83 | --------------- 84 | 1. Enable subsectioning of output data specifically by time rather than index. 85 | 2. Support for DDM file analysis/plotting 86 | 3. Merged input/output display object for 1-command combo plots given proper 87 | inputs. 88 | 4. Get CygnssL2WindDisplay.specular_plot() to automatically adjust size of 89 | points to reflect actual CYGNSS spatial resolution on Basemap. Right now, 90 | user just has manual control of the marker size and would need to guess at 91 | this if they want the specular points to be truly spatially accurate. 92 | 5. Incorporate land/ocean flag in non-Basemap CYGNSS plots 93 | 94 | """ 95 | from __future__ import print_function 96 | import numpy as np 97 | import matplotlib.pyplot as plt 98 | from mpl_toolkits.basemap import Basemap 99 | from netCDF4 import Dataset 100 | from warnings import warn 101 | import os 102 | from six import string_types 103 | import datetime as dt 104 | from sklearn.cluster import DBSCAN 105 | from copy import deepcopy 106 | 107 | VERSION = '0.7' 108 | 109 | ######################### 110 | 111 | 112 | class NetcdfFile(object): 113 | 114 | """Base class used for reading netCDF-format L1 and L2 CYGNSS data files""" 115 | 116 | def __init__(self, filename=None): 117 | try: 118 | self.read_netcdf(filename) 119 | except: 120 | warn('Please provide a correct filename as argument') 121 | 122 | def read_netcdf(self, filename): 123 | """variable_list = holds all the variable key strings """ 124 | volume = Dataset(filename, 'r') 125 | self.filename = os.path.basename(filename) 126 | self.fill_variables(volume) 127 | 128 | def fill_variables(self, volume): 129 | """Loop thru all variables and store them as attributes""" 130 | self.variable_list = [] 131 | for key in volume.variables.keys(): 132 | new_var = np.array(volume.variables[key][:]) 133 | setattr(self, key, new_var) 134 | self.variable_list.append(key) 135 | 136 | ######################### 137 | 138 | 139 | class CygnssSingleSat(NetcdfFile): 140 | 141 | """ 142 | Child class of NetcdfFile. Can ingest both L2 Wind and DDM files. 143 | All variables within the files are incorporated as attributes of the 144 | class. This class forms the main building block of PyGNSS. 145 | """ 146 | 147 | def get_gain_mask(self, number=4): 148 | """ 149 | With L2 wind data, identifies top specular points in terms of 150 | range corrected gain. Creates the GoodData attribute, which provides 151 | a mask that analysis and plotting routines can use to only consider 152 | specular points with the highest RangeCorrectedGain 153 | number = Number of specular points to consider in the rankings 154 | """ 155 | if hasattr(self, 'RangeCorrectedGain'): 156 | self.GoodData = 0 * np.int16(self.RangeCorrectedGain) 157 | indices = np.argsort(self.RangeCorrectedGain, axis=1) 158 | max4 = indices[:, -1*number:] 159 | for i in np.arange(np.shape(self.RangeCorrectedGain)[0]): 160 | self.GoodData[i, max4[i]] = 1 161 | self.variable_list.append('GoodData') 162 | 163 | ######################### 164 | 165 | 166 | class CygnssMultiSat(object): 167 | 168 | """ 169 | Can ingest both L2 Wind and L1 DDM files. Merges the CYGNSS constellation's 170 | individual satellites' data together into a class structure very similar to 171 | CygnssSingleSat, just with bigger array dimensions. 172 | """ 173 | 174 | def __init__(self, l2list, number=4): 175 | """ 176 | l2list = list of CygnssL2SingleSat objects or files 177 | number = Number of maximum RangeCorrectedGain slots to consider 178 | """ 179 | warntxt = 'Requires input list of CygnssSingleSat ' + \ 180 | 'objects or L2 wind files' 181 | try: 182 | test = l2list[0].WindSpeed 183 | except: 184 | try: 185 | if isinstance(l2list[0], string_types): 186 | tmplist = [] 187 | for filen in l2list: 188 | sat = CygnssSingleSat(filen) 189 | sat.get_gain_mask(number=number) 190 | tmplist.append(sat) 191 | l2list = tmplist 192 | else: 193 | warn(warntxt) 194 | return 195 | except: 196 | warn(warntxt) 197 | return 198 | self.satellites = l2list 199 | self.merge_cygnss_data() 200 | 201 | def merge_cygnss_data(self): 202 | """ 203 | Loop over each satellite and append its data to the master arrays 204 | """ 205 | for i, sat in enumerate(self.satellites): 206 | if i == 0: 207 | self.variable_list = sat.variable_list 208 | for var in sat.variable_list: 209 | setattr(self, var, getattr(sat, var)) 210 | else: 211 | for var in sat.variable_list: 212 | array = getattr(sat, var) 213 | if np.ndim(array) == 1: 214 | new_array = np.append(getattr(self, var), array) 215 | setattr(self, var, new_array) 216 | elif np.ndim(array) == 2: 217 | new_array = np.append( 218 | getattr(self, var), array, axis=1) 219 | setattr(self, var, new_array) 220 | else: 221 | pass # for now ... 222 | 223 | ######################### 224 | 225 | 226 | class CygnssL2WindDisplay(object): 227 | 228 | """ 229 | This display class provides an avenue for making plots from CYGNSS L2 wind 230 | data. 231 | """ 232 | 233 | def __init__(self, cygnss_sat_object, number=4): 234 | 235 | """ 236 | cygnss_sat_object = CygnssSingle/MultiSat object, single L2 file, 237 | or list of files. 238 | number = Number of specular points to consider in RangeCorrectedGain 239 | rankings. 240 | """ 241 | # If passed string(s), try to read file(s) & make the wind data object 242 | flag = check_for_strings(cygnss_sat_object) 243 | if flag == 1: 244 | cygnss_sat_object = CygnssSingleSat(cygnss_sat_object) 245 | if flag == 2: 246 | cygnss_sat_object = CygnssMultiSat(cygnss_sat_object, 247 | number=number) 248 | if not hasattr(cygnss_sat_object, 'GoodData'): 249 | try: 250 | cygnss_sat_object.get_gain_mask(number=number) 251 | except: 252 | pass 253 | # Try again to confirm L2, this avoids problems 254 | # caused by ingest of L1 DDM 255 | if not hasattr(cygnss_sat_object, 'GoodData'): 256 | warn('Not a CYGNSS L2 wind object most likely, failing ...') 257 | return 258 | for var in cygnss_sat_object.variable_list: 259 | setattr(self, var, getattr(cygnss_sat_object, var)) 260 | if hasattr(cygnss_sat_object, 'satellites'): 261 | setattr(self, 'satellites', getattr(cygnss_sat_object, 262 | 'satellites')) 263 | self.multi_flag = True 264 | else: 265 | self.multi_flag = False 266 | self.variable_list = cygnss_sat_object.variable_list 267 | 268 | def specular_plot(self, cmap='YlOrRd', title='CYGNSS data', vmin=0, 269 | vmax=30, ms=50, marker='o', bad=-500, fig=None, ax=None, 270 | colorbar_flag=False, basemap=None, edge_flag=False, 271 | axis_label_flag=False, title_flag=True, indices=None, 272 | save=None, lonrange=None, latrange=None, 273 | truth_flag=False, return_flag=False, gpsid=None, 274 | gain=None, sat=None, **kwargs): 275 | """ 276 | Plots CYGNSS specular points on lat/lon axes using matplotlib's scatter 277 | object, which colors each point based on its wind speed value. 278 | 279 | cmap = matplotlib or user-defined colormap 280 | title = Title of plot 281 | vmin = Lowest wind speed value to display on color table 282 | vmax = Highest wind speed value to display on color table 283 | ms = Size of marker used to plot each specular point 284 | marker = Marker shape to use ('o' is best) 285 | bad = Bad value of Lat/Lon to throw out 286 | fig = matplotlib Figure object to use 287 | ax = matplotlib Axes object to use 288 | colorbar_flag = Set to True to show the colorbar 289 | basemap = Basemap object to use in plotting the specular points 290 | edge_flag = Set to True to show a black edge to make each specular 291 | point more distinctive 292 | axis_label_flag = Set to True to label lat/lon axes 293 | title_flag = Set to False to suppress title 294 | indices = Indices (2-element tuple) to use to limit the period of data 295 | shown (i.e., limit by time) 296 | save = Name of image file to save plot to 297 | lonrange = 2-element tuple to limit longitude range of plot 298 | latrange = 2-element tuple to limit latitude range of plot 299 | gpsid = Integer ID number for GPS satellite to examine 300 | sat = CYGNSS satellite number (0-7) 301 | gain = Threshold for range-corrected gain (RCG). Can be 2-ele. tuple. 302 | If so, use only RCG within that range. If scalar, then use 303 | data above the given RCG value. 304 | return_flag = Set to True to return Figure, Axes, and Basemap objects 305 | (in that order) 306 | """ 307 | ds = CygnssSubsection(self, indices=indices, bad=bad, 308 | gpsid=gpsid, gain=gain, sat=sat) 309 | if truth_flag: 310 | ws = ds.tws 311 | else: 312 | ws = ds.ws 313 | if np.size(ds.lon[ds.good]) == 0: 314 | print('No good specular points, not plotting') 315 | return 316 | fig, ax = parse_fig_ax(fig, ax) 317 | if edge_flag: 318 | ec = 'black' 319 | else: 320 | ec = 'none' 321 | if basemap is None: 322 | sc = ax.scatter(ds.lon[ds.good], ds.lat[ds.good], c=ws[ds.good], 323 | vmin=vmin, vmax=vmax, cmap=cmap, s=ms, 324 | marker=marker, edgecolors=ec, **kwargs) 325 | if lonrange is not None: 326 | ax.set_xlim(lonrange) 327 | if latrange is not None: 328 | ax.set_ylim(latrange) 329 | else: 330 | x, y = basemap(ds.lon[ds.good], ds.lat[ds.good]) 331 | sc = basemap.scatter(x, y, c=ws[ds.good], vmin=vmin, vmax=vmax, 332 | cmap=cmap, s=ms, marker=marker, edgecolors=ec, 333 | **kwargs) 334 | if colorbar_flag: 335 | plt.colorbar(sc, label='CYGNSS Wind Speed (m/s)') 336 | if axis_label_flag: 337 | plt.xlabel('Longitude (deg E)') 338 | plt.ylabel('Latitude (deg N)') 339 | if title_flag: 340 | plt.title(title) 341 | if save is not None: 342 | plt.savefig(save) 343 | if return_flag: 344 | return fig, ax, basemap, sc 345 | 346 | def histogram_plot(self, title='CYGNSS Winds vs. True Winds', fig=None, 347 | ax=None, axis_label_flag=False, title_flag=True, 348 | indices=None, bins=10, bad=-500, save=None, 349 | gain=None, sat=None): 350 | """ 351 | Plots a normalized histogram of CYGNSS wind speed vs. true wind speed 352 | (as provided by the input data to the E2ES). 353 | 354 | bins = Number of bins to use in the histogram 355 | title = Title of plot 356 | bad = Bad value of Lat/Lon to throw out 357 | fig = matplotlib Figure object to use 358 | ax = matplotlib Axes object to use 359 | axis_label_flag = Set to True to label lat/lon axes 360 | title_flag = Set to False to suppress title 361 | indices = Indices (2-element tuple) to use to limit the period of data 362 | shown (i.e., limit by time) 363 | gain = Threshold for range-corrected gain (RCG). Can be 2-ele. tuple. 364 | If so, use only RCG within that range. If scalar, then use 365 | data above the given RCG value. 366 | save = Name of image file to save plot to 367 | sat = CYGNSS satellite number (0-7) 368 | """ 369 | ds = CygnssSubsection(self, indices=indices, gain=gain, bad=bad, 370 | sat=sat) 371 | if np.size(ds.lon[ds.good]) == 0: 372 | print('No good specular points, not plotting') 373 | return 374 | fig, ax = parse_fig_ax(fig, ax) 375 | ax.hist(ds.ws[ds.good].ravel()-ds.tws[ds.good].ravel(), bins=bins, 376 | normed=True) 377 | if axis_label_flag: 378 | plt.xlabel('CYGNSS Wind Speed - True Wind Speed (m/s)') 379 | plt.ylabel('Frequency') 380 | if title_flag: 381 | plt.title(title) 382 | if save is not None: 383 | plt.savefig(save) 384 | 385 | def hist2d_plot(self, title='CYGNSS Winds vs. True Winds', fig=None, 386 | ax=None, axis_label_flag=False, title_flag=True, 387 | indices=None, bins=20, bad=-500, save=None, 388 | gain=None, colorbar_flag=True, 389 | cmap='YlOrRd', range=(0, 20), ls='--', 390 | add_line=True, line_color='r', sat=None, 391 | colorbar_label_flag=True, **kwargs): 392 | """ 393 | Plots a normalized 2D histogram of CYGNSS wind speed vs. true wind spd 394 | (as provided by the input data to the E2ES). This information can be 395 | thresholded by RangeCorrectedGain 396 | 397 | bins = Number of bins to use in the histogram 398 | title = Title of plot 399 | bad = Bad value of Lat/Lon to throw out 400 | fig = matplotlib Figure object to use 401 | ax = matplotlib Axes object to use 402 | axis_label_flag = Set to True to label lat/lon axes 403 | title_flag = Set to False to suppress title 404 | indices = Indices (2-element tuple) to use to limit the period of data 405 | shown (i.e., limit by time) 406 | gain = Threshold for range-corrected gain (RCG). Can be 2-ele. tuple. 407 | If so, use only RCG within that range. If scalar, then use 408 | data above the given RCG value. 409 | save = Name of image file to save plot to 410 | sat = CYGNSS satellite number (0-7) 411 | **kwargs = Whatever else pyplot.hist2d will accept 412 | """ 413 | ds = CygnssSubsection(self, indices=indices, gain=gain, 414 | bad=bad, sat=sat) 415 | if np.size(ds.lon[ds.good]) == 0: 416 | print('No good specular points, not plotting') 417 | return 418 | fig, ax = parse_fig_ax(fig, ax) 419 | H, xedges, yedges, img = ax.hist2d( 420 | ds.ws[ds.good].ravel(), ds.tws[ds.good].ravel(), bins=bins, 421 | normed=True, cmap=cmap, zorder=1, range=[range, range], 422 | **kwargs) 423 | # These 2 lines are a hack to get color bar, but not plot anything new 424 | extent = [xedges[0], xedges[-1], yedges[0], yedges[-1]] 425 | im = ax.imshow(H.T, cmap=cmap, extent=extent, zorder=0) 426 | ax.set_xlim(range) 427 | ax.set_ylim(range) 428 | if add_line: 429 | ax.plot(range, range, ls=ls, 430 | color=line_color, lw=2, zorder=2) 431 | if axis_label_flag: 432 | ax.set_xlabel('CYGNSS Wind Speed (m/s)') 433 | ax.set_ylabel('True Wind Speed (m/s)') 434 | if title_flag: 435 | ax.set_title(title) 436 | if colorbar_flag: 437 | if colorbar_label_flag: 438 | label = 'Frequency' 439 | else: 440 | label = '' 441 | plt.colorbar(im, label=label, ax=ax, shrink=0.75) 442 | if save is not None: 443 | plt.savefig(save) 444 | 445 | ######################### 446 | 447 | 448 | class CygnssSubsection(object): 449 | 450 | """ 451 | Class to handle subsectioning CYGNSS data. Subsectioning by 452 | satellite (via CygnssSingleSat input), time indices, GPS satellite ID, 453 | range-corrected gain, etc. is supported. 454 | 455 | Main Attributes 456 | --------------- 457 | ws = Wind speed array 458 | lon = Longitude array 459 | lat = Latitude array 460 | gd = GoodData array 461 | rcg = RangeCorrectedGain array 462 | gps = GpsID array 463 | """ 464 | 465 | def __init__(self, data, indices=None, gpsid=None, gain=None, bad=-500, 466 | sat=None): 467 | """ 468 | data = CygnssSingleSat, CygnssMultiSat, or CygnssL2WindDisplay object 469 | gpsid = Integer ID number for GPS satellite to examine 470 | gain = Threshold by range-corrected gain, values below will be masked 471 | bad = Value to compare against lat/lon to mask out missing data 472 | sat = CYGNSS satellite number (0-7) 473 | indices = Indices (2-element tuple) to use to limit the period of data 474 | shown (i.e., limit by time) 475 | """ 476 | # Set basic attributes based on input data object 477 | if sat is not None and hasattr(data, 'satellites'): 478 | data = data.satellites[sat] 479 | self.ws = data.WindSpeed 480 | self.tws = data.TruthWindSpeed 481 | self.lon = data.Longitude 482 | self.lat = data.Latitude 483 | self.gps = data.GpsID 484 | self.rcg = data.RangeCorrectedGain 485 | self.gd = data.GoodData 486 | 487 | # Set keyword-based attributes 488 | self.gpsid = gpsid 489 | self.gain = gain 490 | self.bad = bad 491 | self.indices = indices 492 | 493 | # Now subsection the data 494 | self.subsection_data() 495 | self.get_good_data_mask() 496 | 497 | def subsection_data(self): 498 | """ 499 | This method subsections the L2 wind data and returns these as arrays 500 | ready to plot. 501 | """ 502 | if self.indices is not None: 503 | self.ws = self.ws[self.indices[0]:self.indices[1]][:] 504 | self.tws = self.tws[self.indices[0]:self.indices[1]][:] 505 | self.lon = self.lon[self.indices[0]:self.indices[1]][:] 506 | self.lat = self.lat[self.indices[0]:self.indices[1]][:] 507 | self.gd = self.gd[self.indices[0]:self.indices[1]][:] 508 | self.gps = self.gps[self.indices[0]:self.indices[1]][:] 509 | self.rcg = self.rcg[self.indices[0]:self.indices[1]][:] 510 | 511 | def get_good_data_mask(self): 512 | """ 513 | Sets a mask used to limit the data plotted. Filtered out are data 514 | masked out by the GoodData mask (based on RangeCorrectedGain), missing 515 | lat/lon values, and bad data (ws < 0) 516 | """ 517 | good1 = np.logical_and(self.gd == 1, self.ws >= 0) 518 | good2 = np.logical_and(self.lon > self.bad, self.lat > self.bad) 519 | if self.gpsid is not None and type(self.gpsid) is int: 520 | good2 = np.logical_and(good2, self.gps == self.gpsid) 521 | if self.gain is not None: 522 | if np.size(self.gain) == 2: 523 | cond = np.logical_and(self.rcg >= self.gain[0], 524 | self.rcg < self.gain[1]) 525 | good2 = np.logical_and(good2, cond) 526 | else: 527 | good2 = np.logical_and(good2, self.rcg >= self.gain) 528 | self.good = np.logical_and(good1, good2) 529 | 530 | ######################### 531 | 532 | 533 | class CygnssTrack(object): 534 | 535 | """ 536 | Class to facilitate extraction of a single track of specular points 537 | from a CygnssSingleSat, CygnssMultiSat, or CygnssL2WindDisplay object. 538 | 539 | Attributes 540 | ---------- 541 | input = CygnssSubsection object 542 | ws = CYGNSS wind speeds 543 | tws = Truth wind speeds 544 | lon = Longitudes of specular points 545 | lat = Latitudes of specular points 546 | rcg = Range-corrected gains of specular points 547 | datetimes = Datetime objects for specular points 548 | 549 | The following attributes are created by filter_track method: 550 | fws = Filtered wind speeds 551 | flon = Filtered longitudes 552 | flat = Filtered latitudes 553 | These attributes are shorter than the main attributes by the window length 554 | """ 555 | 556 | def __init__(self, data, datetimes=None, **kwargs): 557 | """ 558 | data = CygnssSingleSat, CygnssMultiSat, or CygnssL2WindDisplay object 559 | datetimes = List of datetime objects from get_datetime function. 560 | If None, this function is called. 561 | """ 562 | self.input = CygnssSubsection(data, **kwargs) 563 | self.ws = self.input.ws[self.input.good] 564 | self.tws = self.input.tws[self.input.good] 565 | self.lon = self.input.lon[self.input.good] 566 | self.lat = self.input.lat[self.input.good] 567 | self.rcg = self.input.rcg[self.input.good] 568 | if datetimes is None: 569 | dts = get_datetime(data) 570 | else: 571 | dts = datetimes 572 | if self.input.indices is not None: 573 | self.datetimes = dts[ 574 | self.input.indices[0]:self.input.indices[1]][self.input.good] 575 | else: 576 | self.datetimes = dts[self.input.good] 577 | 578 | def filter_track(self, window=5): 579 | """ 580 | Applies a running-mean filter to the track. 581 | 582 | window = Number of specular points in the running mean window. 583 | Must be odd. 584 | """ 585 | if window % 2 == 0: 586 | raise ValueError('Window must be odd length, not even.') 587 | hl = int((window - 1) / 2) 588 | self.fws = np.convolve( 589 | self.ws, np.ones((window,))/window, mode='valid') 590 | self.flon = self.lon[hl:-1*hl] 591 | self.flat = self.lat[hl:-1*hl] 592 | 593 | ######################### 594 | 595 | 596 | class E2esInputData(NetcdfFile): 597 | 598 | """Base class for ingesting E2ES input data. Child class of NetcdfFile.""" 599 | 600 | def get_wind_speed(self): 601 | """ 602 | Input E2ES data normally don't have wind speed as a field. This method 603 | fixes that. 604 | """ 605 | self.WindSpeed = np.sqrt(self.eastward_wind**2 + 606 | self.northward_wind**2) 607 | self.variable_list.append('WindSpeed') 608 | 609 | ######################### 610 | 611 | 612 | class InputWindDisplay(object): 613 | 614 | """Display object for the E2ES input data""" 615 | 616 | def __init__(self, input_winds_object): 617 | """ 618 | input_winds_object = Input E2esInputData object or wind file 619 | """ 620 | # If passed a string, try to read the file & make input data object 621 | if isinstance(input_winds_object, string_types): 622 | input_winds_object = E2esInputData(input_winds_object) 623 | if not hasattr(input_winds_object, 'WindSpeed'): 624 | input_winds_object.get_wind_speed() 625 | for var in input_winds_object.variable_list: 626 | setattr(self, var, getattr(input_winds_object, var)) 627 | self.make_coordinates_2d() 628 | 629 | def basemap_plot(self, fill_color='#ACACBF', ax=None, fig=None, 630 | time_index=0, cmap='YlOrRd', vmin=0, vmax=30, 631 | colorbar_flag=True, return_flag=True, save=None, 632 | title='Input Wind Speed', title_flag=True, 633 | show_grid=False): 634 | """ 635 | Plots E2ES input wind speed data on a Basemap using matplotlib's 636 | pcolormesh object. Defaults to return the Basemap object so other 637 | things (e.g., CYGNSS data) can be overplotted. 638 | 639 | fill_color = Color to fill continents 640 | time_index = If the input data contain more than one time step, this 641 | index selects the time step to display 642 | cmap = matplotlib or user-defined colormap 643 | title = Title of plot 644 | vmin = Lowest wind speed value to display on color table 645 | vmax = Highest wind speed value to display on color table 646 | fig = matplotlib Figure object to use 647 | ax = matplotlib Axes object to use 648 | return_flag = Set to False to suppress Basemap object return 649 | title_flag = Set to False to suppress title 650 | save = Name of image file to save plot to 651 | colorbar_flag = Set to False to suppress the colorbar 652 | show_grid = Set to True to show the lat/lon grid and label it 653 | """ 654 | fig, ax = parse_fig_ax(fig, ax) 655 | m = get_basemap(lonrange=[np.min(self.longitude), 656 | np.max(self.longitude)], 657 | latrange=[np.min(self.latitude), 658 | np.max(self.latitude)]) 659 | m.fillcontinents(color=fill_color) 660 | x, y = m(self.longitude, self.latitude) 661 | cs = m.pcolormesh(x, y, self.WindSpeed[time_index], 662 | vmin=vmin, vmax=vmax, cmap=cmap) 663 | if show_grid: 664 | m.drawmeridians( 665 | np.arange(-180, 180, 5), labels=[True, True, True, True]) 666 | m.drawparallels( 667 | np.arange(-90, 90, 5), labels=[True, True, True, True]) 668 | if title_flag: 669 | plt.title(title) 670 | if colorbar_flag: 671 | m.colorbar(cs, label='Wind Speed (m/s)', location='bottom', 672 | pad="7%") 673 | if save is not None: 674 | plt.savefig(save) 675 | if return_flag: 676 | return m 677 | 678 | def make_coordinates_2d(self): 679 | if np.ndim(self.longitude) == 1: 680 | lon2d, lat2d = np.meshgrid(self.longitude, self.latitude) 681 | self.longitude = lon2d 682 | self.latitude = lat2d 683 | 684 | ############################## 685 | # Independent Functions Follow 686 | ############################## 687 | 688 | 689 | def get_tracks(data, indices=None, min_samples=10, verbose=False, 690 | filter=False, window=5): 691 | """ 692 | Returns a list of CygnssTrack objects from a CYGNSS data or display object 693 | 694 | data = CygnssSingleSat, CygnssMultiSat, or CygnssL2WindDisplay object 695 | indices = Indices (2-element tuple) to use to limit the period of data 696 | shown (i.e., limit by time). Not usually necessary unless 697 | processing more than one day's worth of data. 698 | min_samples = Minimum allowable track size (number of specular points) 699 | verbose = Set to True for some text updates while running 700 | filter = Set to True to filter each track 701 | window = Window length of filter, in # of specular points. Must be odd. 702 | """ 703 | trl = [] 704 | dts = get_datetime(data) 705 | # For some reason range works but np.arange doesn't. 706 | for csat in range(8): 707 | if not hasattr(data, 'satellites'): 708 | if csat > 0: 709 | break 710 | if verbose: 711 | print('CYGNSS satellite', csat) 712 | for gsat in range(np.max(data.GpsID)+1): 713 | # This will isolate most tracks, improving later cluster analysis 714 | ds = CygnssTrack(data, datetimes=dts, indices=indices, gpsid=gsat, 715 | sat=csat) 716 | if np.size(ds.lon) > 0: 717 | # Cluster analysis separates out any remaining grouped tracks 718 | X = list(zip(ds.lon, ds.lat)) 719 | db = DBSCAN(min_samples=min_samples).fit(X) 720 | labels = db.labels_ 721 | uniq = np.unique(labels) 722 | for element in uniq[uniq >= 0]: 723 | # A bit clunky, but make a copy of the CygnssTrack object 724 | # to help separate out remaining tracks in the scene 725 | dsc = deepcopy(ds) 726 | dsc.lon = ds.lon[labels == element] 727 | dsc.lat = ds.lat[labels == element] 728 | dsc.ws = ds.ws[labels == element] 729 | dsc.tws = ds.tws[labels == element] 730 | dsc.rcg = ds.lon[labels == element] 731 | dsc.datetimes = ds.datetimes[labels == element] 732 | dsc.sat = csat 733 | dsc.prn = gsat 734 | trl.append(dsc) 735 | if filter: 736 | for tr in trl: 737 | tr.filter_track(window=window) 738 | return trl 739 | 740 | 741 | def get_datetime(data): 742 | if hasattr(data, 'satellites'): 743 | data = data.satellites[0] 744 | dts = [] 745 | for i in np.arange(len(data.Year)): 746 | dti = dt.datetime(data.Year[i], data.Month[i], data.Day[i], 747 | data.Hour[i], data.Minute[i], data.Second[i]) 748 | tmplist = [dti for i in np.arange(15)] 749 | dts.append(tmplist) 750 | return np.array(dts) 751 | 752 | 753 | def parse_fig_ax(fig, ax): 754 | """ 755 | Parse matplotlib Figure and Axes objects, if provided, or just grab the 756 | current ones in memory. 757 | """ 758 | if fig is None: 759 | fig = plt.gcf() 760 | if ax is None: 761 | ax = plt.gca() 762 | return fig, ax 763 | 764 | 765 | def get_basemap(latrange=[-90, 90], lonrange=[-180, 180], resolution='l', 766 | area_thresh=1000): 767 | """ 768 | Function to create a specifically formatted Basemap provided the input 769 | parameters. 770 | 771 | latrange = Latitude range of the plot (2-element tuple) 772 | lonrange = Longitude range of the plot (2-element tuple) 773 | resolution = Resolution of the Basemap 774 | area_thresh = Threshold (in km^**2) for displaying small features, such as 775 | lakes/islands 776 | """ 777 | lon_0 = np.mean(lonrange) 778 | lat_0 = np.mean(latrange) 779 | m = Basemap(projection='merc', lon_0=lon_0, lat_0=lat_0, lat_ts=lat_0, 780 | llcrnrlat=np.min(latrange), urcrnrlat=np.max(latrange), 781 | llcrnrlon=np.min(lonrange), urcrnrlon=np.max(lonrange), 782 | rsphere=6371200., resolution=resolution, 783 | area_thresh=area_thresh) 784 | m.drawcoastlines() 785 | m.drawstates() 786 | m.drawcountries() 787 | return m 788 | 789 | 790 | def check_for_strings(var): 791 | """ 792 | Given an input var, check to see if it is a string (scalar or array of 793 | strings), or something else. 794 | 795 | Output: 796 | 0 = non-string, 1 = string scalar, 2 = string array 797 | """ 798 | if np.size(var) == 1: 799 | if isinstance(var, string_types): 800 | return 1 801 | else: 802 | return 0 803 | else: 804 | for val in var: 805 | if not isinstance(val, string_types): 806 | return 0 807 | return 2 808 | -------------------------------------------------------------------------------- /pygnss/orbit.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import datetime as dt 3 | from copy import deepcopy 4 | import xarray 5 | from sklearn.cluster import DBSCAN 6 | import h5py 7 | import scipy 8 | import os 9 | 10 | array_list = ['lon', 'lat', 'ws', 'rcg', 'ws_yslf_nbrcs', 11 | 'ws_yslf_les', 'datetimes', 'sod'] 12 | scalar_list = ['antenna', 'prn', 'sat'] 13 | 14 | 15 | def read_cygnss_l2(fname): 16 | return xarray.open_dataset(fname) 17 | 18 | 19 | def read_imerg(fname): 20 | return Imerg(fname) 21 | 22 | 23 | def split_tracks_in_time(track, gap=60): 24 | """ 25 | This function will split a CygnssTrack object into two separate tracks 26 | if there is a significant gap in time. Currently can split a track up to 27 | three times. 28 | 29 | Parameters 30 | ---------- 31 | track : CygnssTrack object 32 | CYGNSS track that needs to be checked for breaks in time 33 | 34 | Other Parameters 35 | ---------------- 36 | gap : int 37 | Number of seconds in a gap before a split is forced 38 | 39 | Returns 40 | ------- 41 | track_list : list 42 | List of CygnssTrack objects broken up from original track 43 | """ 44 | indices = np.where(np.diff(track.sod) > gap)[0] 45 | if len(indices) > 0: 46 | if len(indices) == 1: 47 | track1 = subset_track(deepcopy(track), 0, indices[0]+1) 48 | track2 = subset_track(deepcopy(track), indices[0]+1, 49 | len(track.sod)) 50 | return [track1, track2] 51 | if len(indices) == 2: 52 | track1 = subset_track(deepcopy(track), 0, indices[0]+1) 53 | track2 = subset_track(deepcopy(track), indices[0]+1, 54 | indices[1]+1) 55 | track3 = subset_track(deepcopy(track), indices[1]+1, 56 | len(track.sod)) 57 | return [track1, track2, track3] 58 | if len(indices) == 3: 59 | track1 = subset_track(deepcopy(track), 0, indices[0]+1) 60 | track2 = subset_track(deepcopy(track), indices[0]+1, indices[1]+1) 61 | track3 = subset_track(deepcopy(track), indices[1]+1, indices[2]+1) 62 | track4 = subset_track(deepcopy(track), indices[2]+1, 63 | len(track.sod)) 64 | return [track1, track2, track3, track4] 65 | else: 66 | print('Found more than four tracks!') 67 | return 0 68 | 69 | 70 | def subset_track(track, index1, index2): 71 | """ 72 | This function subsets a CYGNSS track to only include data from a range 73 | defined by two indexes. 74 | 75 | Parameters 76 | ---------- 77 | track : CygnssTrack object 78 | CygnssTrack to be subsetted 79 | index1 : int 80 | Starting index 81 | index2 : int 82 | Ending index 83 | 84 | Returns 85 | ------- 86 | track : CygnssTrack object 87 | Subsetted CygnssTrack object 88 | """ 89 | for arr in array_list: 90 | setattr(track, arr, getattr(track, arr)[index1:index2]) 91 | for scalar in scalar_list: 92 | setattr(track, scalar, getattr(track, scalar)) 93 | return track 94 | 95 | 96 | def get_tracks(data, sat, min_samples=10, verbose=False, 97 | filter=False, window=5, eps=1, gap=60): 98 | """ 99 | Returns a list of isolated CygnssTrack objects from a CYGNSS data object. 100 | 101 | Parameters 102 | ---------- 103 | data : xarray.core.dataset.Dataset object 104 | CYGNSS data object as read by xarray.open_dataset 105 | sat : int 106 | CYGNSS satellite to be analyzed. 107 | 108 | Other Parameters 109 | ---------------- 110 | min_samples : int 111 | Minimum allowable track size (number of specular points) 112 | verbose : bool 113 | True - Provide text updates while running 114 | 115 | False - Don't do this 116 | 117 | filter : bool 118 | True - Each track will receive a filter 119 | 120 | False - Don't do this 121 | 122 | window : int 123 | Window length of filter, in number of specular points. Must be odd. 124 | eps : scalar 125 | This is the eps keyword to be passed to DBSCAN. It is the max distance 126 | (in degrees lat/lon) between two tracks for them to be considered as 127 | part of the same track. 128 | gap : int 129 | Number of seconds in a track gap before a split is forced 130 | 131 | Returns 132 | ------- 133 | trl : list 134 | List of isolated CygnssTrack objects 135 | """ 136 | trl = [] 137 | dts = get_datetime(data) 138 | # Currently only works for one satellite at a time due to resource issues 139 | if type(sat) is not int or sat < 1 or sat > 8: 140 | raise ValueError('sat must be integer between 1 and 8') 141 | else: 142 | csat = sat 143 | if verbose: 144 | print('CYGNSS satellite', csat) 145 | print('GPS code (max =', str(int(np.max(data.prn_code.data)))+'):', 146 | end=' ') 147 | for gsat in range(np.int16(np.max(data.prn_code.data)+1)): 148 | if verbose: 149 | print(gsat, end=' ') 150 | # This will isolate most tracks, improving later cluster analysis 151 | for ant in range(np.int16(np.max(data.antenna.data)+1)): 152 | ds = CygnssTrack(data, datetimes=dts, gpsid=gsat, 153 | sat=csat, antenna=ant) 154 | if np.size(ds.lon) > 0: 155 | # Cluster analysis separates out additional grouped tracks 156 | # Only simplistic analysis of lat/lon gaps in degrees needed 157 | X = list(zip(ds.lon, ds.lat)) 158 | db = DBSCAN(min_samples=min_samples, eps=eps).fit(X) 159 | labels = db.labels_ 160 | uniq = np.unique(labels) 161 | for element in uniq[uniq >= 0]: 162 | # A bit clunky, but make a copy of the CygnssTrack object 163 | # to help separate out remaining tracks in the scene 164 | dsc = deepcopy(ds) 165 | for key in array_list: 166 | setattr(dsc, key, getattr(ds, key)[labels == element]) 167 | dsc.lon[dsc.lon > 180] -= 360.0 168 | for key in scalar_list: 169 | setattr(dsc, key, np.array(getattr(ds, key))[0]) 170 | # Final separation by splitting about major time gaps 171 | test = split_tracks_in_time(dsc, gap=gap) 172 | if test is None: # No time gap, append the original track 173 | trl.append(dsc) 174 | # Failsafe - Ignore difficult-to-split combined tracks 175 | elif test == 0: 176 | pass 177 | else: # Loop thru split-up tracks and append separately 178 | for t in test: 179 | trl.append(t) 180 | del dsc 181 | del db, labels, uniq, X 182 | del ds # This function is a resource hog, forcing some cleanup 183 | if filter: 184 | for tr in trl: 185 | tr.filter_track(window=window) 186 | return trl 187 | 188 | 189 | def get_datetime(cyg): 190 | epoch_start = np.datetime64('1970-01-01T00:00:00Z') 191 | tdelta = np.timedelta64(1, 's') 192 | return np.array([dt.datetime.utcfromtimestamp((st - epoch_start) / tdelta) 193 | for st in cyg.sample_time.data]) 194 | 195 | 196 | class CygnssSubsection(object): 197 | 198 | """ 199 | Class to handle subsectioning CYGNSS data. Subsectioning by 200 | satellite (via CygnssSingleSat input), time indices, GPS satellite ID, 201 | range-corrected gain, etc. is supported. 202 | 203 | Main Attributes 204 | --------------- 205 | ws = Wind speed array 206 | lon = Longitude array 207 | lat = Latitude array 208 | rcg = RangeCorrectedGain array 209 | gps = GpsID array 210 | """ 211 | 212 | def __init__(self, data, gpsid=None, gain=None, sat=None, antenna=None): 213 | """ 214 | data = CygnssSingleSat, CygnssMultiSat, or CygnssL2WindDisplay object 215 | gpsid = Integer ID number for GPS satellite to examine 216 | gain = Threshold by range-corrected gain, values below will be masked 217 | bad = Value to compare against lat/lon to mask out missing data 218 | sat = CYGNSS satellite number (1-8) 219 | """ 220 | # Set basic attributes based on input data object 221 | self.ws = data.wind_speed.data 222 | self.ws_yslf_nbrcs = data.yslf_nbrcs_wind_speed.data 223 | self.ws_yslf_les = data.yslf_les_wind_speed.data 224 | self.lon = data.lon.data 225 | self.lat = data.lat.data 226 | self.gps = np.int16(data.prn_code.data) 227 | self.antenna = np.int16(data.antenna.data) 228 | self.rcg = data.range_corr_gain.data 229 | self.cygnum = np.int16(data.spacecraft_num.data) 230 | 231 | # Set keyword-based attributes 232 | self.gpsid = gpsid 233 | self.gain = gain 234 | self.sat = sat 235 | self.ant_num = antenna 236 | 237 | # Now subsection the data 238 | self.get_good_data_mask() 239 | 240 | def get_good_data_mask(self): 241 | """ 242 | Sets a mask used to limit the data plotted. Filtered out are data 243 | masked out by the GoodData mask (based on RangeCorrectedGain), missing 244 | lat/lon values, and bad data (ws < 0) 245 | """ 246 | good1 = self.ws >= 0 247 | good2 = np.logical_and(np.isfinite(self.lon), np.isfinite(self.lat)) 248 | if self.gpsid is not None and type(self.gpsid) is int: 249 | good2 = np.logical_and(good2, self.gps == self.gpsid) 250 | if self.gain is not None: 251 | if np.size(self.gain) == 2: 252 | cond = np.logical_and(self.rcg >= self.gain[0], 253 | self.rcg < self.gain[1]) 254 | good2 = np.logical_and(good2, cond) 255 | else: 256 | good2 = np.logical_and(good2, self.rcg >= self.gain) 257 | if self.sat is not None and type(self.sat) is int: 258 | good2 = np.logical_and(good2, self.cygnum == self.sat) 259 | if self.ant_num is not None and type(self.sat) is int: 260 | good2 = np.logical_and(good2, self.antenna == self.ant_num) 261 | self.good = np.logical_and(good1, good2) 262 | 263 | 264 | class CygnssTrack(object): 265 | 266 | """ 267 | Class to facilitate extraction of a single track of specular points 268 | from a CygnssSingleSat, CygnssMultiSat, or CygnssL2WindDisplay object. 269 | 270 | Attributes 271 | ---------- 272 | input = CygnssSubsection object 273 | ws = CYGNSS wind speeds 274 | lon = Longitudes of specular points 275 | lat = Latitudes of specular points 276 | rcg = Range-corrected gains of specular points 277 | datetimes = Datetime objects for specular points 278 | 279 | The following attributes are created by filter_track method: 280 | fws = Filtered wind speeds 281 | flon = Filtered longitudes 282 | flat = Filtered latitudes 283 | These attributes are shorter than the main attributes by the window length 284 | """ 285 | 286 | def __init__(self, data, datetimes=None, **kwargs): 287 | """ 288 | data = CygnssSingleSat, CygnssMultiSat, or CygnssL2WindDisplay object 289 | datetimes = List of datetime objects from get_datetime function. 290 | If None, this function is called. 291 | """ 292 | self.input = CygnssSubsection(data, **kwargs) 293 | self.ws = self.input.ws[self.input.good] 294 | self.ws_yslf_nbrcs = self.input.ws_yslf_nbrcs[self.input.good] 295 | self.ws_yslf_les = self.input.ws_yslf_les[self.input.good] 296 | self.lon = self.input.lon[self.input.good] 297 | self.lat = self.input.lat[self.input.good] 298 | self.rcg = self.input.rcg[self.input.good] 299 | self.antenna = self.input.antenna[self.input.good] 300 | self.prn = self.input.gps[self.input.good] 301 | self.sat = self.input.cygnum[self.input.good] 302 | if datetimes is None: 303 | dts = get_datetime(data) 304 | else: 305 | dts = datetimes 306 | self.datetimes = dts[self.input.good] 307 | sod = [] 308 | for dt1 in self.datetimes: 309 | sod.append((dt1 - dt.datetime( 310 | self.datetimes[0].year, self.datetimes[0].month, 311 | self.datetimes[0].day)).total_seconds()) 312 | self.sod = np.array(sod) 313 | 314 | def filter_track(self, window=5): 315 | """ 316 | Applies a running-mean filter to the track. 317 | 318 | window = Number of specular points in the running mean window. 319 | Must be odd. 320 | """ 321 | if window % 2 == 0: 322 | raise ValueError('Window must be odd length, not even.') 323 | hl = int((window - 1) / 2) 324 | self.fws = np.convolve( 325 | self.ws, np.ones((window,))/window, mode='valid') 326 | self.flon = self.lon[hl:-1*hl] 327 | self.flat = self.lat[hl:-1*hl] 328 | 329 | 330 | class Imerg(object): 331 | 332 | def __init__(self, filen): 333 | self.read_imerg(filen) 334 | 335 | def read_imerg(self, filen): 336 | imerg = h5py.File(filen, 'r') 337 | self.datetime = dt.datetime.strptime(os.path.basename(filen)[23:39], 338 | '%Y%m%d-S%H%M%S') 339 | self.precip = np.ma.masked_where( 340 | np.transpose(imerg['Grid']['precipitationCal']) <= 0, 341 | np.transpose(imerg['Grid']['precipitationCal'])) 342 | self.lon = np.array(imerg['Grid']['lon']) 343 | self.lat = np.array(imerg['Grid']['lat']) 344 | self.filename = os.path.basename(filen) 345 | imerg.close() 346 | 347 | def downsample(self): 348 | filled_precip = self.precip.filled(fill_value=0.0) 349 | dummy = scipy.ndimage.interpolation.zoom(filled_precip, 0.5) 350 | self.coarse_precip = np.ma.masked_where(dummy <= 0, dummy) 351 | self.coarse_lon = self.lon[::2] 352 | self.coarse_lat = self.lat[::2] 353 | 354 | 355 | def add_imerg(trl, ifiles, dt_imerg): 356 | for ii in range(len(trl)): 357 | check_dt = trl[ii].datetimes[len(trl[ii].sod)//2] 358 | # diff = np.abs(check_dt - dt_imerg) 359 | index = np.where(dt_imerg <= check_dt)[0][-1] # np.argmin(diff) 360 | imerg = Imerg(ifiles[index]) 361 | if ii % 50 == 0: 362 | print(ii, end=' ') 363 | precip = [] 364 | for j in range(len(trl[ii].lon)): 365 | ilon = int(np.round((trl[ii].lon[j] - imerg.lon[0]) / 0.10)) 366 | ilat = int(np.round((trl[ii].lat[j] - imerg.lat[0]) / 0.10)) 367 | precip.append(imerg.precip[ilat, ilon]) 368 | precip = np.array(precip) 369 | precip[~np.isfinite(precip)] = 0.0 370 | setattr(trl[ii], 'precip', precip) 371 | setattr(trl[ii], 'imerg', os.path.basename(ifiles[index])) 372 | print() 373 | return trl 374 | 375 | 376 | def write_netcdfs(trl, path): 377 | for i, track in enumerate(trl): 378 | fname = 'track_' + str(track.sat).zfill(2) + '_' + \ 379 | str(track.prn).zfill(2) + \ 380 | '_' + str(track.antenna).zfill(2) + '_' + str(i).zfill(4) + \ 381 | track.datetimes[0].strftime('_%Y%m%d_s%H%M%S_') + \ 382 | track.datetimes[-1].strftime('e%H%M%S.nc') 383 | ds = xarray.Dataset( 384 | {'ws': (['nt'], track.ws), 385 | 'ws_yslf_nbrcs': (['nt'], track.ws_yslf_nbrcs), 386 | 'ws_yslf_les': (['nt'], track.ws_yslf_les), 387 | 'lat': (['nt'], track.lat), 388 | 'lon': (['nt'], track.lon), 389 | 'datetimes': (['nt'], track.datetimes), 390 | 'rcg': (['nt'], track.rcg), 391 | 'precip': (['nt'], track.precip), 392 | 'sod': (['nt'], track.sod)}, 393 | coords={'nt': (['nt'], np.arange(len(track.ws)))}, 394 | attrs={'imerg': track.imerg}) 395 | ds.to_netcdf(path + fname, format='NETCDF3_CLASSIC') 396 | ds.close() 397 | del(ds) 398 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='PyGNSS', 5 | version='0.7', 6 | author='Timothy Lang', 7 | author_email='timothy.j.lang@nasa.gov', 8 | packages=['pygnss', ], 9 | description='Python Interface to Cyclone Global Navigation Satellite ' + 10 | 'System (CYGNSS) Wind Dataset', 11 | classifiers=[ 12 | "Development Status :: 3 - Alpha", 13 | "Environment :: Console" 14 | ], 15 | ) 16 | --------------------------------------------------------------------------------