├── .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 |
Hoover, K. E., J. R. Mecikalski, T. J. Lang, X. Li, T. J. Castillo, and T. Chronis, 2018: Use of an End-to-End-Simulator to analyze CYGNSS. J. Atmos. Ocean. Technol., doi: 10.1175/JTECH-D-17-0036.1.
19 | Ruf, C. S., Chew, C., Lang, T., Morris, M. G., Nave, K., Ridley, A., & Balasubramaniam, R. (2018). A New Paradigm in Earth Environmental Monitoring with the CYGNSS Small Satellite Constellation. Scientific reports, 8(1), 8782.
20 | Lang, T.J. Comparing Winds Near Tropical Oceanic Precipitation Systems with and without Lightning. Remote Sens. 2020, 12, 3968. https://doi.org/10.3390/rs12233968
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 |
--------------------------------------------------------------------------------